diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-12 14:27:29 +0200 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-10-13 09:35:20 +0000 |
commit | c30a6232df03e1efbd9f3b226777b07e087a1122 (patch) | |
tree | e992f45784689f373bcc38d1b79a239ebe17ee23 /chromium/content/browser/accessibility | |
parent | 7b5b123ac58f58ffde0f4f6e488bcd09aa4decd3 (diff) | |
download | qtwebengine-chromium-85-based.tar.gz |
BASELINE: Update Chromium to 85.0.4183.14085-based
Change-Id: Iaa42f4680837c57725b1344f108c0196741f6057
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/content/browser/accessibility')
51 files changed, 3542 insertions, 755 deletions
diff --git a/chromium/content/browser/accessibility/accessibility_auralinux_browsertest.cc b/chromium/content/browser/accessibility/accessibility_auralinux_browsertest.cc index 4b64570478c..71a757df88e 100644 --- a/chromium/content/browser/accessibility/accessibility_auralinux_browsertest.cc +++ b/chromium/content/browser/accessibility/accessibility_auralinux_browsertest.cc @@ -60,6 +60,8 @@ class AccessibilityAuraLinuxBrowserTest : public AccessibilityBrowserTest { return false; } + // Ensures that the text and the start and end offsets retrieved using + // get_textAtOffset match the expected values. static void CheckTextAtOffset(AtkText* text_object, int offset, AtkTextBoundary boundary_type, @@ -67,54 +69,69 @@ class AccessibilityAuraLinuxBrowserTest : public AccessibilityBrowserTest { int expected_end_offset, const char* expected_text); + // Loads a page with an input text field and places sample text in it. + // Returns a pointer to the field's AtkText interface. AtkText* SetUpInputField(); + + // Loads a page with a textarea text field, places sample text in it, and + // places the caret after the last character. + // Returns a pointer to the field's AtkText interface. AtkText* SetUpTextareaField(); + + // Loads a page with a paragraph of sample text and returns its AtkText + // interface. AtkText* SetUpSampleParagraph(); - AtkText* SetUpSampleParagraphInScrollableDocument(); + // Retrieves a pointer to the already loaded paragraph's AtkText interface. AtkText* GetSampleParagraph(); - AtkText* GetAtkTextForChild(AtkRole expected_role); + + // Searches the accessibility tree in pre-order debth-first traversal for a + // node with the given role and returns its AtkText interface if found, + // otherwise returns nullptr. + AtkText* FindNode(const AtkRole role); private: + // Searches the accessibility tree in pre-order debth-first traversal starting + // at a given node and for a node with the given role and returns its AtkText + // interface if found, otherwise returns nullptr. + AtkText* FindNode(AtkObject* root, const AtkRole role) const; + DISALLOW_COPY_AND_ASSIGN(AccessibilityAuraLinuxBrowserTest); }; -AtkText* AccessibilityAuraLinuxBrowserTest::GetAtkTextForChild( - AtkRole expected_role) { - AtkObject* document = GetRendererAccessible(); - EXPECT_EQ(1, atk_object_get_n_accessible_children(document)); - - AtkObject* parent_element = atk_object_ref_accessible_child(document, 0); - int number_of_children = atk_object_get_n_accessible_children(parent_element); - EXPECT_LT(0, number_of_children); - - // The input field is always the last child. - AtkObject* input = - atk_object_ref_accessible_child(parent_element, number_of_children - 1); - EXPECT_EQ(expected_role, atk_object_get_role(input)); - - EXPECT_TRUE(ATK_IS_TEXT(input)); - AtkText* atk_text = ATK_TEXT(input); - - g_object_unref(parent_element); +void AccessibilityAuraLinuxBrowserTest::CheckTextAtOffset( + AtkText* text_object, + int offset, + AtkTextBoundary boundary_type, + int expected_start_offset, + int expected_end_offset, + const char* expected_text) { + testing::Message message; + message << "While checking at index \'" << offset << "\' for \'" + << expected_text << "\' at " << expected_start_offset << '-' + << expected_end_offset << '.'; + SCOPED_TRACE(message); - return atk_text; + int start_offset = 0; + int end_offset = 0; + char* text = atk_text_get_text_at_offset(text_object, offset, boundary_type, + &start_offset, &end_offset); + EXPECT_EQ(expected_start_offset, start_offset); + EXPECT_EQ(expected_end_offset, end_offset); + EXPECT_STREQ(expected_text, text); + g_free(text); } -// Loads a page with an input text field and places sample text in it. AtkText* AccessibilityAuraLinuxBrowserTest::SetUpInputField() { LoadInputField(); - return GetAtkTextForChild(ATK_ROLE_ENTRY); + return FindNode(ATK_ROLE_ENTRY); } -// Loads a page with a textarea text field and places sample text in it. Also, -// places the caret before the last character. AtkText* AccessibilityAuraLinuxBrowserTest::SetUpTextareaField() { LoadTextareaField(); - return GetAtkTextForChild(ATK_ROLE_ENTRY); + return FindNode(ATK_ROLE_ENTRY); } -// Loads a page with a paragraph of sample text. AtkText* AccessibilityAuraLinuxBrowserTest::SetUpSampleParagraph() { LoadSampleParagraph(); @@ -139,37 +156,48 @@ AtkText* AccessibilityAuraLinuxBrowserTest::GetSampleParagraph() { int number_of_children = atk_object_get_n_accessible_children(document); EXPECT_LT(0, number_of_children); - // The input field is always the last child. - AtkObject* input = atk_object_ref_accessible_child(document, 0); - EXPECT_EQ(ATK_ROLE_PARAGRAPH, atk_object_get_role(input)); + // The paragraph is the last child. + AtkObject* paragraph = atk_object_ref_accessible_child(document, 0); + EXPECT_EQ(ATK_ROLE_PARAGRAPH, atk_object_get_role(paragraph)); - EXPECT_TRUE(ATK_IS_TEXT(input)); - return ATK_TEXT(input); + EXPECT_TRUE(ATK_IS_TEXT(paragraph)); + return ATK_TEXT(paragraph); } -// Ensures that the text and the start and end offsets retrieved using -// get_textAtOffset match the expected values. -void AccessibilityAuraLinuxBrowserTest::CheckTextAtOffset( - AtkText* text_object, - int offset, - AtkTextBoundary boundary_type, - int expected_start_offset, - int expected_end_offset, - const char* expected_text) { - testing::Message message; - message << "While checking at index \'" << offset << "\' for \'" - << expected_text << "\' at " << expected_start_offset << '-' - << expected_end_offset << '.'; - SCOPED_TRACE(message); +AtkText* AccessibilityAuraLinuxBrowserTest::FindNode(const AtkRole role) { + AtkObject* document = GetRendererAccessible(); + EXPECT_NE(nullptr, document); + return FindNode(document, role); +} - int start_offset = 0; - int end_offset = 0; - char* text = atk_text_get_text_at_offset(text_object, offset, boundary_type, - &start_offset, &end_offset); - EXPECT_EQ(expected_start_offset, start_offset); - EXPECT_EQ(expected_end_offset, end_offset); - EXPECT_STREQ(expected_text, text); - g_free(text); +AtkText* AccessibilityAuraLinuxBrowserTest::FindNode(AtkObject* root, + const AtkRole role) const { + EXPECT_NE(nullptr, root); + if (atk_object_get_role(root) == role) { + EXPECT_TRUE(ATK_IS_TEXT(root)); + g_object_ref(root); + AtkText* root_text = ATK_TEXT(root); + return root_text; + } + + for (int i = 0; i < atk_object_get_n_accessible_children(root); ++i) { + AtkObject* child = atk_object_ref_accessible_child(root, i); + EXPECT_NE(nullptr, child); + if (atk_object_get_role(child) == role) { + EXPECT_TRUE(ATK_IS_TEXT(child)); + AtkText* child_text = ATK_TEXT(child); + return child_text; + } + + if (AtkText* descendant_text = FindNode(child, role)) { + g_object_unref(child); + return descendant_text; + } + + g_object_unref(child); + } + + return nullptr; } IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest, @@ -228,7 +256,7 @@ IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest, // "Before". // // The embedded object character representing the image is at offset 6. - for (int i = 0; i <= 6; ++i) { + for (int i = 0; i < 6; ++i) { CheckTextAtOffset(contenteditable_text, i, ATK_TEXT_BOUNDARY_CHAR, i, (i + 1), expected_hypertext[i].c_str()); } @@ -1142,6 +1170,126 @@ IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest, g_object_unref(atk_text); } +IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest, + SetSelectionWithIgnoredObjects) { + LoadInitialAccessibilityTreeFromHtml(R"HTML(<!DOCTYPE html> + <html> + <body> + <ul> + <li> + <div role="presentation"></div> + <p role="presentation"> + <span>Banana</span> + </p> + <span>fruit.</span> + </li> + </ul> + </body> + </html>)HTML"); + + AtkText* atk_list_item = FindNode(ATK_ROLE_LIST_ITEM); + ASSERT_NE(nullptr, atk_list_item); + + // The hypertext expose by "list_item_text" includes an embedded object + // character for the list bullet and the joined word "Bananafruit.". The word + // "Banana" is exposed as text because its container paragraph is ignored. + int n_characters = atk_text_get_character_count(atk_list_item); + ASSERT_EQ(13, n_characters); + + AccessibilityNotificationWaiter waiter( + shell()->web_contents(), ui::kAXModeComplete, + ax::mojom::Event::kDocumentSelectionChanged); + + // First select the whole of the text found in the hypertext. + int start_offset = 0; + int end_offset = n_characters; + std::string embedded_character; + ASSERT_TRUE( + base::UTF16ToUTF8(&ui::AXPlatformNodeAuraLinux::kEmbeddedCharacter, 1, + &embedded_character)); + char* selected_text = nullptr; + + EXPECT_TRUE( + atk_text_set_selection(atk_list_item, 0, start_offset, end_offset)); + waiter.WaitForNotification(); + + selected_text = + atk_text_get_selection(atk_list_item, 0, &start_offset, &end_offset); + ASSERT_NE(nullptr, selected_text); + EXPECT_EQ(0, start_offset); + EXPECT_EQ(n_characters, end_offset); + // The list bullet should be represented by an embedded object character. + EXPECT_STREQ((embedded_character + std::string("Bananafruit.")).c_str(), + selected_text); + g_free(selected_text); + + // Select only the list bullet. + start_offset = 0; + end_offset = 1; + EXPECT_TRUE( + atk_text_set_selection(atk_list_item, 0, start_offset, end_offset)); + waiter.WaitForNotification(); + + selected_text = + atk_text_get_selection(atk_list_item, 0, &start_offset, &end_offset); + ASSERT_NE(nullptr, selected_text); + EXPECT_EQ(0, start_offset); + EXPECT_EQ(1, end_offset); + // The list bullet should be represented by an embedded object character. + EXPECT_STREQ(embedded_character.c_str(), selected_text); + g_free(selected_text); + + // Select the word "Banana" in the ignored paragraph. + start_offset = 1; + end_offset = 7; + EXPECT_TRUE( + atk_text_set_selection(atk_list_item, 0, start_offset, end_offset)); + waiter.WaitForNotification(); + + selected_text = + atk_text_get_selection(atk_list_item, 0, &start_offset, &end_offset); + ASSERT_NE(nullptr, selected_text); + EXPECT_EQ(1, start_offset); + EXPECT_EQ(7, end_offset); + EXPECT_STREQ("Banana", selected_text); + g_free(selected_text); + + // Select both the list bullet and the word "Banana" in the ignored paragraph. + start_offset = 0; + end_offset = 7; + EXPECT_TRUE( + atk_text_set_selection(atk_list_item, 0, start_offset, end_offset)); + waiter.WaitForNotification(); + + selected_text = + atk_text_get_selection(atk_list_item, 0, &start_offset, &end_offset); + ASSERT_NE(nullptr, selected_text); + EXPECT_EQ(0, start_offset); + EXPECT_EQ(7, end_offset); + // The list bullet should be represented by an embedded object character. + EXPECT_STREQ((embedded_character + std::string("Banana")).c_str(), + selected_text); + g_free(selected_text); + + // Select the joined word "Bananafruit." both in the ignored paragraph and in + // the unignored span. + start_offset = 1; + end_offset = n_characters; + EXPECT_TRUE( + atk_text_set_selection(atk_list_item, 0, start_offset, end_offset)); + waiter.WaitForNotification(); + + selected_text = + atk_text_get_selection(atk_list_item, 0, &start_offset, &end_offset); + ASSERT_NE(nullptr, selected_text); + EXPECT_EQ(1, start_offset); + EXPECT_EQ(n_characters, end_offset); + EXPECT_STREQ("Bananafruit.", selected_text); + g_free(selected_text); + + g_object_unref(atk_list_item); +} + IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest, TestAtkTextListItem) { LoadInitialAccessibilityTreeFromHtml( R"HTML(<!DOCTYPE html> @@ -1731,4 +1879,68 @@ IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest, } } +IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest, + HitTestOnAncestorOfWebRoot) { + // Load the page. + LoadInitialAccessibilityTreeFromHtml(R"HTML( + <button>This is a button</button> + )HTML"); + + WebContentsImpl* web_contents = + static_cast<WebContentsImpl*>(shell()->web_contents()); + BrowserAccessibilityManager* manager = + web_contents->GetRootBrowserAccessibilityManager(); + + // Find a node to hit test. Note that this is a really simple page, + // so synchronous hit testing will work fine. + BrowserAccessibility* node = manager->GetRoot(); + while (node && node->GetRole() != ax::mojom::Role::kButton) + node = manager->NextInTreeOrder(node); + DCHECK(node); + + // Get the screen bounds of the hit target and find the point in the middle. + gfx::Rect bounds = node->GetClippedScreenBoundsRect(); + gfx::Point point = bounds.CenterPoint(); + + // Get the root AXPlatformNodeAuraLinux. + ui::AXPlatformNodeAuraLinux* root_platform_node = + static_cast<ui::AXPlatformNodeAuraLinux*>( + ui::AXPlatformNode::FromNativeViewAccessible( + manager->GetRoot()->GetNativeViewAccessible())); + + // First test that calling accHitTest on the root node returns the button. + { + gfx::NativeViewAccessible hit_child = root_platform_node->HitTestSync( + point.x(), point.y(), AtkCoordType::ATK_XY_SCREEN); + ASSERT_NE(nullptr, hit_child); + ui::AXPlatformNode* hit_child_node = + ui::AXPlatformNode::FromNativeViewAccessible(hit_child); + ASSERT_NE(nullptr, hit_child_node); + EXPECT_EQ(node->GetId(), hit_child_node->GetDelegate()->GetData().id); + } + + // Now test it again, but this time caliing accHitTest on the parent + // IAccessible of the web root node. + { + RenderWidgetHostViewAura* rwhva = static_cast<RenderWidgetHostViewAura*>( + shell()->web_contents()->GetRenderWidgetHostView()); + gfx::NativeViewAccessible ancestor = rwhva->GetParentNativeViewAccessible(); + + ASSERT_NE(nullptr, ancestor); + + ui::AXPlatformNodeAuraLinux* ancestor_node = + static_cast<ui::AXPlatformNodeAuraLinux*>( + ui::AXPlatformNode::FromNativeViewAccessible(ancestor)); + ASSERT_NE(nullptr, ancestor_node); + + gfx::NativeViewAccessible hit_child = ancestor_node->HitTestSync( + point.x(), point.y(), AtkCoordType::ATK_XY_SCREEN); + ASSERT_NE(nullptr, hit_child); + ui::AXPlatformNode* hit_child_node = + ui::AXPlatformNode::FromNativeViewAccessible(hit_child); + ASSERT_NE(nullptr, hit_child_node); + EXPECT_EQ(node->GetId(), hit_child_node->GetDelegate()->GetData().id); + } +} + } // namespace content diff --git a/chromium/content/browser/accessibility/accessibility_event_recorder_uia_win.cc b/chromium/content/browser/accessibility/accessibility_event_recorder_uia_win.cc index 5e5895010af..8f52006bc78 100644 --- a/chromium/content/browser/accessibility/accessibility_event_recorder_uia_win.cc +++ b/chromium/content/browser/accessibility/accessibility_event_recorder_uia_win.cc @@ -20,6 +20,7 @@ #include "content/browser/accessibility/browser_accessibility_com_win.h" #include "content/browser/accessibility/browser_accessibility_manager.h" #include "content/browser/accessibility/browser_accessibility_manager_win.h" +#include "ui/accessibility/platform/uia_registrar_win.h" #include "ui/base/win/atl_module.h" namespace content { @@ -110,14 +111,8 @@ void AccessibilityEventRecorderUia::Thread::ThreadMain() { CHECK(uia_.Get()); // Register the custom event to mark the end of the test. - Microsoft::WRL::ComPtr<IUIAutomationRegistrar> registrar; - CoCreateInstance(CLSID_CUIAutomationRegistrar, NULL, CLSCTX_INPROC_SERVER, - IID_IUIAutomationRegistrar, ®istrar); - CHECK(registrar.Get()); - UIAutomationEventInfo custom_event = {kUiaTestCompleteSentinelGuid, - kUiaTestCompleteSentinel}; - CHECK( - SUCCEEDED(registrar->RegisterEvent(&custom_event, &shutdown_sentinel_))); + shutdown_sentinel_ = + ui::UiaRegistrarWin::GetInstance().GetUiaTestCompleteEventId(); // Find the IUIAutomationElement for the root content window uia_->ElementFromHandle(hwnd_, &root_); diff --git a/chromium/content/browser/accessibility/accessibility_tree_formatter_auralinux.cc b/chromium/content/browser/accessibility/accessibility_tree_formatter_auralinux.cc index 85a308d7591..b09bf27719f 100644 --- a/chromium/content/browser/accessibility/accessibility_tree_formatter_auralinux.cc +++ b/chromium/content/browser/accessibility/accessibility_tree_formatter_auralinux.cc @@ -21,8 +21,6 @@ #include "content/browser/accessibility/accessibility_tree_formatter_utils_auralinux.h" #include "content/browser/accessibility/browser_accessibility_auralinux.h" #include "ui/accessibility/platform/ax_platform_node_auralinux.h" -#include "ui/base/x/x11_util.h" -#include "ui/gfx/x/x11.h" namespace content { diff --git a/chromium/content/browser/accessibility/accessibility_tree_formatter_base.cc b/chromium/content/browser/accessibility/accessibility_tree_formatter_base.cc index 14ccfb92527..706b036cab9 100644 --- a/chromium/content/browser/accessibility/accessibility_tree_formatter_base.cc +++ b/chromium/content/browser/accessibility/accessibility_tree_formatter_base.cc @@ -8,6 +8,7 @@ #include <memory> #include <utility> +#include <vector> #include "base/check_op.h" #include "base/strings/pattern.h" @@ -32,6 +33,235 @@ const char kSkipChildren[] = "@NO_CHILDREN_DUMP"; } // namespace +// +// PropertyNode +// + +// static +PropertyNode PropertyNode::FromPropertyFilter( + const AccessibilityTreeFormatter::PropertyFilter& filter) { + // Property invocation: property_str expected format is + // prop_name or prop_name(arg1, ... argN). + PropertyNode root; + Parse(&root, filter.property_str.begin(), filter.property_str.end()); + + PropertyNode* node = &root.parameters[0]; + node->original_property = filter.property_str; + + // Line indexes filter: filter_str expected format is + // :line_num_1, ... :line_num_N, a comma separated list of line indexes + // the property should be queried for. For example, ":1,:5,:7" indicates that + // the property should called for objects placed on 1, 5 and 7 lines only. + if (!filter.filter_str.empty()) { + node->line_indexes = + base::SplitString(filter.filter_str, base::string16(1, ','), + base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); + } + + return std::move(*node); +} + +PropertyNode::PropertyNode() = default; +PropertyNode::PropertyNode(PropertyNode&& o) + : key(std::move(o.key)), + name_or_value(std::move(o.name_or_value)), + parameters(std::move(o.parameters)), + original_property(std::move(o.original_property)), + line_indexes(std::move(o.line_indexes)) {} +PropertyNode::~PropertyNode() = default; + +PropertyNode& PropertyNode::operator=(PropertyNode&& o) { + key = std::move(o.key); + name_or_value = std::move(o.name_or_value); + parameters = std::move(o.parameters); + original_property = std::move(o.original_property); + line_indexes = std::move(o.line_indexes); + return *this; +} + +PropertyNode::operator bool() const { + return !name_or_value.empty(); +} + +bool PropertyNode::IsArray() const { + return name_or_value == base::ASCIIToUTF16("[]"); +} + +bool PropertyNode::IsDict() const { + return name_or_value == base::ASCIIToUTF16("{}"); +} + +base::Optional<int> PropertyNode::AsInt() const { + int value = 0; + if (!base::StringToInt(name_or_value, &value)) { + return base::nullopt; + } + return value; +} + +base::Optional<base::string16> PropertyNode::FindKey(const char* refkey) const { + for (const auto& param : parameters) { + if (param.key == base::ASCIIToUTF16(refkey)) { + return param.name_or_value; + } + } + return base::nullopt; +} + +base::Optional<int> PropertyNode::FindIntKey(const char* refkey) const { + for (const auto& param : parameters) { + if (param.key == base::ASCIIToUTF16(refkey)) { + return param.AsInt(); + } + } + return base::nullopt; +} + +std::string PropertyNode::ToString() const { + std::string out; + for (const auto& index : line_indexes) { + if (!out.empty()) { + out += ','; + } + out += base::UTF16ToUTF8(index); + } + if (!out.empty()) { + out += ';'; + } + + if (!key.empty()) { + out += base::UTF16ToUTF8(key) + ": "; + } + out += base::UTF16ToUTF8(name_or_value); + if (parameters.size()) { + out += '('; + for (size_t i = 0; i < parameters.size(); i++) { + if (i != 0) { + out += ", "; + } + out += parameters[i].ToString(); + } + out += ')'; + } + return out; +} + +// private +PropertyNode::PropertyNode(PropertyNode::iterator key_begin, + PropertyNode::iterator key_end, + const base::string16& name_or_value) + : key(key_begin, key_end), name_or_value(name_or_value) {} +PropertyNode::PropertyNode(PropertyNode::iterator begin, + PropertyNode::iterator end) + : name_or_value(begin, end) {} +PropertyNode::PropertyNode(PropertyNode::iterator key_begin, + PropertyNode::iterator key_end, + PropertyNode::iterator value_begin, + PropertyNode::iterator value_end) + : key(key_begin, key_end), name_or_value(value_begin, value_end) {} + +// private static +PropertyNode::iterator PropertyNode::Parse(PropertyNode* node, + PropertyNode::iterator begin, + PropertyNode::iterator end) { + auto iter = begin; + auto key_begin = end, key_end = end; + while (iter != end) { + // Subnode begins: create a new node, record its name and parse its + // arguments. + if (*iter == '(') { + node->parameters.push_back(PropertyNode(key_begin, key_end, begin, iter)); + key_begin = key_end = end; + begin = iter = Parse(&node->parameters.back(), ++iter, end); + continue; + } + + // Subnode begins: a special case for arrays, which have [arg1, ..., argN] + // form. + if (*iter == '[') { + node->parameters.push_back( + PropertyNode(key_begin, key_end, base::UTF8ToUTF16("[]"))); + key_begin = key_end = end; + begin = iter = Parse(&node->parameters.back(), ++iter, end); + continue; + } + + // Subnode begins: a special case for dictionaries of {key1: value1, ..., + // key2: value2} form. + if (*iter == '{') { + node->parameters.push_back( + PropertyNode(key_begin, key_end, base::UTF8ToUTF16("{}"))); + key_begin = key_end = end; + begin = iter = Parse(&node->parameters.back(), ++iter, end); + continue; + } + + // Subnode ends. + if (*iter == ')' || *iter == ']' || *iter == '}') { + if (begin != iter) { + node->parameters.push_back( + PropertyNode(key_begin, key_end, begin, iter)); + key_begin = key_end = end; + } + return ++iter; + } + + // Dictionary key + auto maybe_key_end = end; + if (*iter == ':') { + maybe_key_end = iter++; + } + + // Skip spaces, adjust new node start. + if (*iter == ' ') { + if (maybe_key_end != end) { + key_begin = begin; + key_end = maybe_key_end; + } + begin = ++iter; + continue; + } + + // Subsequent scalar param case. + if (*iter == ',' && begin != iter) { + node->parameters.push_back(PropertyNode(key_begin, key_end, begin, iter)); + iter++; + key_begin = key_end = end; + begin = iter; + continue; + } + + iter++; + } + + // Single scalar param case. + if (begin != iter) { + node->parameters.push_back(PropertyNode(begin, iter)); + } + return iter; +} + +// +// AccessibilityTreeFormatter +// + +AccessibilityTreeFormatter::PropertyFilter::PropertyFilter( + const PropertyFilter&) = default; + +AccessibilityTreeFormatter::PropertyFilter::PropertyFilter( + const base::string16& str, + Type type) + : match_str(str), type(type) { + size_t index = str.find(';'); + if (index != std::string::npos) { + filter_str = str.substr(0, index); + if (index + 1 < str.length()) { + match_str = str.substr(index + 1, std::string::npos); + } + } + property_str = match_str.substr(0, match_str.find('=')); +} + AccessibilityTreeFormatter::TestPass AccessibilityTreeFormatter::GetTestPass( size_t index) { std::vector<content::AccessibilityTreeFormatter::TestPass> passes = @@ -189,26 +419,38 @@ AccessibilityTreeFormatterBase::GetVersionSpecificExpectedFileSuffix() { return FILE_PATH_LITERAL(""); } -bool AccessibilityTreeFormatterBase::FilterPropertyName( - const base::string16& text) { - // Find the first allow-filter matching the property name. The filter should - // be either an exact property match or a wildcard matching to support filter - // collections like AXRole* which matches AXRoleDescription. - const base::string16 delim = base::ASCIIToUTF16("="); +PropertyNode AccessibilityTreeFormatterBase::GetMatchingPropertyNode( + const base::string16& line_index, + const base::string16& property_name) { + // Find the first allow-filter matching the line index and the property name. for (const auto& filter : property_filters_) { - base::String16Tokenizer tokenizer(filter.match_str, delim); - if (tokenizer.GetNext() && (text == tokenizer.token() || - base::MatchPattern(text, tokenizer.token()))) { + PropertyNode property_node = PropertyNode::FromPropertyFilter(filter); + + // Skip if the line index filter doesn't matched (if specified). + if (!property_node.line_indexes.empty() && + std::find(property_node.line_indexes.begin(), + property_node.line_indexes.end(), + line_index) == property_node.line_indexes.end()) { + continue; + } + + // The filter should be either an exact property match or a wildcard + // matching to support filter collections like AXRole* which matches + // AXRoleDescription. + if (property_name == property_node.name_or_value || + base::MatchPattern(property_name, property_node.name_or_value)) { switch (filter.type) { case PropertyFilter::ALLOW_EMPTY: case PropertyFilter::ALLOW: - return true; + return property_node; + case PropertyFilter::DENY: + break; default: break; } } } - return false; + return PropertyNode(); } bool AccessibilityTreeFormatterBase::MatchesPropertyFilters( @@ -282,4 +524,5 @@ void AccessibilityTreeFormatterBase::AddPropertyFilter( void AccessibilityTreeFormatterBase::AddDefaultFilters( std::vector<PropertyFilter>* property_filters) {} + } // namespace content diff --git a/chromium/content/browser/accessibility/accessibility_tree_formatter_base.h b/chromium/content/browser/accessibility/accessibility_tree_formatter_base.h index 702bcd6811a..13ebfad47d3 100644 --- a/chromium/content/browser/accessibility/accessibility_tree_formatter_base.h +++ b/chromium/content/browser/accessibility/accessibility_tree_formatter_base.h @@ -27,6 +27,70 @@ const char kChildrenDictAttr[] = "children"; namespace content { +// Property node is a tree-like structure, representing a property or collection +// of properties and its invocation parameters. A collection of properties is +// specified by putting a wildcard into a property name, for exampe, AXRole* +// will match both AXRole and AXRoleDescription properties. Parameters of a +// property are given in parentheses like a conventional function call, for +// example, AXCellForColumnAndRow([0, 0]) will call AXCellForColumnAndRow +// parameterized property for column/row 0 indexes. +class CONTENT_EXPORT PropertyNode final { + public: + // Parses a property node from a string. + static PropertyNode FromPropertyFilter( + const AccessibilityTreeFormatter::PropertyFilter& filter); + + PropertyNode(); + PropertyNode(PropertyNode&&); + ~PropertyNode(); + + PropertyNode& operator=(PropertyNode&& other); + explicit operator bool() const; + + // Key name in case of { key: value } dictionary. + base::string16 key; + + // Value or a property name, for example 3 or AXLineForIndex + base::string16 name_or_value; + + // Parameters if it's a property, for example, it is a vector of a single + // value 3 in case of AXLineForIndex(3) + std::vector<PropertyNode> parameters; + + // Used to store the origianl unparsed property including invocation + // parameters if any. + base::string16 original_property; + + // The list of line indexes of accessible objects the property is allowed to + // be called for. + std::vector<base::string16> line_indexes; + + // Argument conversion methods. + bool IsArray() const; + bool IsDict() const; + base::Optional<int> AsInt() const; + base::Optional<base::string16> FindKey(const char* refkey) const; + base::Optional<int> FindIntKey(const char* key) const; + + std::string ToString() const; + + private: + using iterator = base::string16::const_iterator; + + explicit PropertyNode(iterator key_begin, + iterator key_end, + const base::string16&); + PropertyNode(iterator begin, iterator end); + PropertyNode(iterator key_begin, + iterator key_end, + iterator value_begin, + iterator value_end); + + // Builds a property node struct for a string of NAME(ARG1, ..., ARGN) format, + // where each ARG is a scalar value or a string of the same format. + static iterator Parse(PropertyNode* node, iterator begin, iterator end); +}; + // A utility class for formatting platform-specific accessibility information, // for use in testing, debugging, and developer tools. // This is extended by a subclass for each platform where accessibility is @@ -86,8 +150,11 @@ class CONTENT_EXPORT AccessibilityTreeFormatterBase // Overridden by platform subclasses. // - // Returns true if the property name matches a property filter. - bool FilterPropertyName(const base::string16& text); + // Returns a property node struct built for a matching property filter, + // which includes a property name and invocation parameters if any. + // If no matching property filter, then empty property node is returned. + PropertyNode GetMatchingPropertyNode(const base::string16& line_index, + const base::string16& property_name); // Process accessibility tree with filters for output. // Given a dictionary that contains a platform-specific dictionary diff --git a/chromium/content/browser/accessibility/accessibility_tree_formatter_base_unittest.cc b/chromium/content/browser/accessibility/accessibility_tree_formatter_base_unittest.cc new file mode 100644 index 00000000000..a2d5fdfb547 --- /dev/null +++ b/chromium/content/browser/accessibility/accessibility_tree_formatter_base_unittest.cc @@ -0,0 +1,100 @@ +// Copyright (c) 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/browser/accessibility/accessibility_tree_formatter_base.h" + +#include "content/browser/accessibility/browser_accessibility.h" +#include "content/browser/accessibility/browser_accessibility_manager.h" +#include "content/browser/accessibility/test_browser_accessibility_delegate.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "ui/accessibility/ax_enums.mojom.h" + +namespace content { + +class AccessibilityTreeFormatterBaseTest : public testing::Test { + public: + AccessibilityTreeFormatterBaseTest() = default; + ~AccessibilityTreeFormatterBaseTest() override = default; + + protected: + std::unique_ptr<TestBrowserAccessibilityDelegate> + test_browser_accessibility_delegate_; + + private: + void SetUp() override { + test_browser_accessibility_delegate_ = + std::make_unique<TestBrowserAccessibilityDelegate>(); + } + + DISALLOW_COPY_AND_ASSIGN(AccessibilityTreeFormatterBaseTest); +}; + +PropertyNode Parse(const char* input) { + AccessibilityTreeFormatter::PropertyFilter filter( + base::UTF8ToUTF16(input), + AccessibilityTreeFormatter::PropertyFilter::ALLOW); + return PropertyNode::FromPropertyFilter(filter); +} + +PropertyNode GetArgumentNode(const char* input) { + auto got = Parse(input); + if (got.parameters.size() == 0) { + return PropertyNode(); + } + return std::move(got.parameters[0]); +} + +void ParseAndCheck(const char* input, const char* expected) { + auto got = Parse(input).ToString(); + EXPECT_EQ(got, expected); +} + +TEST_F(AccessibilityTreeFormatterBaseTest, ParseProperty) { + // Properties and methods. + ParseAndCheck("Role", "Role"); + ParseAndCheck("ChildAt(3)", "ChildAt(3)"); + ParseAndCheck("Cell(3, 4)", "Cell(3, 4)"); + ParseAndCheck("Volume(3, 4, 5)", "Volume(3, 4, 5)"); + ParseAndCheck("TableFor(CellBy(id))", "TableFor(CellBy(id))"); + ParseAndCheck("A(B(1), 2)", "A(B(1), 2)"); + ParseAndCheck("A(B(1), 2, C(3, 4))", "A(B(1), 2, C(3, 4))"); + ParseAndCheck("[3, 4]", "[](3, 4)"); + ParseAndCheck("Cell([3, 4])", "Cell([](3, 4))"); + + // Arguments + ParseAndCheck("Text({val: 1})", "Text({}(val: 1))"); + ParseAndCheck("Text({lat: 1, len: 1})", "Text({}(lat: 1, len: 1))"); + ParseAndCheck("Text({dict: {val: 1}})", "Text({}(dict: {}(val: 1)))"); + ParseAndCheck("Text({dict: {val: 1}, 3})", "Text({}(dict: {}(val: 1), 3))"); + ParseAndCheck("Text({dict: [1, 2]})", "Text({}(dict: [](1, 2)))"); + ParseAndCheck("Text({dict: ValueFor(1)})", "Text({}(dict: ValueFor(1)))"); + + // Line indexes filter. + ParseAndCheck(":3,:5;AXDOMClassList", ":3,:5;AXDOMClassList"); + + // Wrong format. + ParseAndCheck("Role(3", "Role(3)"); + ParseAndCheck("TableFor(CellBy(id", "TableFor(CellBy(id))"); + ParseAndCheck("[3, 4", "[](3, 4)"); + + // Arguments conversion + EXPECT_EQ(GetArgumentNode("ChildAt([3])").IsArray(), true); + EXPECT_EQ(GetArgumentNode("Text({loc: 3, len: 2})").IsDict(), true); + EXPECT_EQ(GetArgumentNode("ChildAt(3)").IsDict(), false); + EXPECT_EQ(GetArgumentNode("ChildAt(3)").IsArray(), false); + EXPECT_EQ(GetArgumentNode("ChildAt(3)").AsInt(), 3); + EXPECT_EQ(GetArgumentNode("Text({start: :1, dir: forward})").FindKey("start"), + base::ASCIIToUTF16(":1")); + EXPECT_EQ(GetArgumentNode("Text({start: :1, dir: forward})").FindKey("dir"), + base::ASCIIToUTF16("forward")); + EXPECT_EQ( + GetArgumentNode("Text({start: :1, dir: forward})").FindKey("notexists"), + base::nullopt); + EXPECT_EQ(GetArgumentNode("Text({loc: 3, len: 2})").FindIntKey("loc"), 3); + EXPECT_EQ(GetArgumentNode("Text({loc: 3, len: 2})").FindIntKey("len"), 2); + EXPECT_EQ(GetArgumentNode("Text({loc: 3, len: 2})").FindIntKey("notexists"), + base::nullopt); +} + +} // namespace content diff --git a/chromium/content/browser/accessibility/accessibility_tree_formatter_blink.cc b/chromium/content/browser/accessibility/accessibility_tree_formatter_blink.cc index 7bf60d5408d..abc989bacd5 100644 --- a/chromium/content/browser/accessibility/accessibility_tree_formatter_blink.cc +++ b/chromium/content/browser/accessibility/accessibility_tree_formatter_blink.cc @@ -170,6 +170,8 @@ void AccessibilityTreeFormatterBlink::AddDefaultFilters( AddPropertyFilter(property_filters, "protected"); AddPropertyFilter(property_filters, "required"); AddPropertyFilter(property_filters, "select*"); + AddPropertyFilter(property_filters, "selectedFromFocus=*", + PropertyFilter::DENY); AddPropertyFilter(property_filters, "visited"); // Other attributes AddPropertyFilter(property_filters, "busy=true"); diff --git a/chromium/content/browser/accessibility/accessibility_tree_formatter_mac.mm b/chromium/content/browser/accessibility/accessibility_tree_formatter_mac.mm index 7388129d79b..bc23c9cea29 100644 --- a/chromium/content/browser/accessibility/accessibility_tree_formatter_mac.mm +++ b/chromium/content/browser/accessibility/accessibility_tree_formatter_mac.mm @@ -41,116 +41,38 @@ const char kHeightDictAttr[] = "height"; const char kRangeLocDictAttr[] = "loc"; const char kRangeLenDictAttr[] = "len"; -std::unique_ptr<base::DictionaryValue> PopulatePosition( - const BrowserAccessibility& node) { - BrowserAccessibilityManager* root_manager = node.manager()->GetRootManager(); - DCHECK(root_manager); - - std::unique_ptr<base::DictionaryValue> position(new base::DictionaryValue); - // The NSAccessibility position of an object is in global coordinates and - // based on the lower-left corner of the object. To make this easier and less - // confusing, convert it to local window coordinates using the top-left - // corner when dumping the position. - BrowserAccessibility* root = root_manager->GetRoot(); - BrowserAccessibilityCocoa* cocoa_root = ToBrowserAccessibilityCocoa(root); - NSPoint root_position = [[cocoa_root position] pointValue]; - NSSize root_size = [[cocoa_root size] sizeValue]; - int root_top = -static_cast<int>(root_position.y + root_size.height); - int root_left = static_cast<int>(root_position.x); - - BrowserAccessibilityCocoa* cocoa_node = - ToBrowserAccessibilityCocoa(const_cast<BrowserAccessibility*>(&node)); - NSPoint node_position = [[cocoa_node position] pointValue]; - NSSize node_size = [[cocoa_node size] sizeValue]; - - position->SetInteger(kXCoordDictAttr, - static_cast<int>(node_position.x - root_left)); - position->SetInteger( - kYCoordDictAttr, - static_cast<int>(-node_position.y - node_size.height - root_top)); - return position; -} - -std::unique_ptr<base::DictionaryValue> PopulateSize( - const BrowserAccessibilityCocoa* cocoa_node) { - std::unique_ptr<base::DictionaryValue> size(new base::DictionaryValue); - NSSize node_size = [[cocoa_node size] sizeValue]; - size->SetInteger(kHeightDictAttr, static_cast<int>(node_size.height)); - size->SetInteger(kWidthDictAttr, static_cast<int>(node_size.width)); - return size; -} - -std::unique_ptr<base::DictionaryValue> PopulateRange(NSRange range) { - std::unique_ptr<base::DictionaryValue> rangeDict(new base::DictionaryValue); - rangeDict->SetInteger(kRangeLocDictAttr, static_cast<int>(range.location)); - rangeDict->SetInteger(kRangeLenDictAttr, static_cast<int>(range.length)); - return rangeDict; -} - -// Returns true if |value| is an NSValue containing a NSRange. -bool IsRangeValue(id value) { - if (![value isKindOfClass:[NSValue class]]) - return false; - return 0 == strcmp([value objCType], @encode(NSRange)); -} - -std::unique_ptr<base::Value> PopulateObject(id value); - -std::unique_ptr<base::ListValue> PopulateArray(NSArray* array) { - std::unique_ptr<base::ListValue> list(new base::ListValue); - for (NSUInteger i = 0; i < [array count]; i++) - list->Append(PopulateObject([array objectAtIndex:i])); - return list; -} - -std::unique_ptr<base::Value> StringForBrowserAccessibility( - BrowserAccessibilityCocoa* obj) { - NSMutableArray* tokens = [[NSMutableArray alloc] init]; - - // Always include the role - id role = [obj role]; - [tokens addObject:role]; - - // If the role is "group", include the role description as well. - id roleDescription = [obj roleDescription]; - if ([role isEqualToString:NSAccessibilityGroupRole] && - roleDescription != nil && ![roleDescription isEqualToString:@""] && - ![roleDescription isEqualToString:@"group"]) { - [tokens addObject:roleDescription]; - } - - // Include the description, title, or value - the first one not empty. - id title = [obj title]; - id description = [obj descriptionForAccessibility]; - id value = [obj value]; - if (description && ![description isEqual:@""]) { - [tokens addObject:description]; - } else if (title && ![title isEqual:@""]) { - [tokens addObject:title]; - } else if (value && ![value isEqual:@""]) { - [tokens addObject:value]; - } - - NSString* result = [tokens componentsJoinedByString:@" "]; - return std::unique_ptr<base::Value>( - new base::Value(SysNSStringToUTF16(result))); -} - -std::unique_ptr<base::Value> PopulateObject(id value) { - if ([value isKindOfClass:[NSArray class]]) - return std::unique_ptr<base::Value>(PopulateArray((NSArray*)value)); - if (IsRangeValue(value)) - return std::unique_ptr<base::Value>(PopulateRange([value rangeValue])); - if ([value isKindOfClass:[BrowserAccessibilityCocoa class]]) { - std::string str; - StringForBrowserAccessibility(value)->GetAsString(&str); - return std::unique_ptr<base::Value>( - StringForBrowserAccessibility((BrowserAccessibilityCocoa*)value)); - } - - return std::unique_ptr<base::Value>(new base::Value( - SysNSStringToUTF16([NSString stringWithFormat:@"%@", value]))); -} +const char kSetKeyPrefixDictAttr[] = "_setkey_"; +const char kConstValuePrefix[] = "_const_"; +const char kNULLValue[] = "_const_NULL"; +const char kFailedToParseArgsError[] = "_const_ERROR:FAILED_TO_PARSE_ARGS"; + +#define INT_FAIL(propnode, msg) \ + LOG(ERROR) << "Failed to parse " << propnode.original_property \ + << " to Int: " << msg; \ + return nil; + +#define INTARRAY_FAIL(propnode, msg) \ + LOG(ERROR) << "Failed to parse " << propnode.original_property \ + << " to IntArray: " << msg; \ + return nil; + +#define NSRANGE_FAIL(propnode, msg) \ + LOG(ERROR) << "Failed to parse " << propnode.original_property \ + << " to NSRange: " << msg; \ + return nil; + +#define UIELEMENT_FAIL(propnode, msg) \ + LOG(ERROR) << "Failed to parse " << propnode.original_property \ + << " to UIElement: " << msg; \ + return nil; + +#define TEXTMARKER_FAIL(propnode, msg) \ + LOG(ERROR) << "Failed to parse " << propnode.original_property \ + << " to AXTextMarker: " << msg \ + << ". Expected format: {anchor, offset, affinity}, where anchor " \ + "is :line_num, offset is integer, affinity is either down, " \ + "up or none"; \ + return nil; } // namespace @@ -173,19 +95,79 @@ class AccessibilityTreeFormatterMac : public AccessibilityTreeFormatterBase { const base::StringPiece& pattern) override; private: + using LineIndexesMap = + std::map<const gfx::NativeViewAccessible, base::string16>; + void RecursiveBuildAccessibilityTree(const BrowserAccessibilityCocoa* node, + const LineIndexesMap& line_indexes_map, base::DictionaryValue* dict); + void RecursiveBuildLineIndexesMap(const BrowserAccessibilityCocoa* node, + LineIndexesMap* line_indexes_map, + int* counter); base::FilePath::StringType GetExpectedFileSuffix() override; const std::string GetAllowEmptyString() override; const std::string GetAllowString() override; const std::string GetDenyString() override; const std::string GetDenyNodeString() override; + void AddProperties(const BrowserAccessibilityCocoa* node, - base::DictionaryValue* dict); + const LineIndexesMap& line_indexes_map, + base::Value* dict); + + // Helper class used to compute a parameter for a parameterized attribute + // call. Can be either id or error. Similar to base::Optional, but allows nil + // id as a valid value. + class IdOrError { + public: + IdOrError() : value(nil), error(false) {} + + IdOrError& operator=(id other_value) { + error = !other_value; + value = other_value; + return *this; + } + + bool IsError() const { return error; } + bool IsNotNil() const { return !!value; } + constexpr const id& operator*() const& { return value; } + + private: + id value; + bool error; + }; + + IdOrError ParamByPropertyNode(const PropertyNode&, + const LineIndexesMap&) const; + NSNumber* PropertyNodeToInt(const PropertyNode&) const; + NSArray* PropertyNodeToIntArray(const PropertyNode&) const; + NSValue* PropertyNodeToRange(const PropertyNode&) const; + gfx::NativeViewAccessible PropertyNodeToUIElement( + const PropertyNode&, + const LineIndexesMap&) const; + id PropertyNodeToTextMarker(const PropertyNode&, const LineIndexesMap&) const; + + base::Value PopulateSize(const BrowserAccessibilityCocoa*) const; + base::Value PopulatePosition(const BrowserAccessibilityCocoa*) const; + base::Value PopulateRange(NSRange) const; + base::Value PopulateTextPosition( + BrowserAccessibilityPosition::AXPositionInstance::pointer, + const LineIndexesMap&) const; + base::Value PopulateTextMarkerRange(id, const LineIndexesMap&) const; + base::Value PopulateObject(id, const LineIndexesMap& line_indexes_map) const; + base::Value PopulateArray(NSArray*, + const LineIndexesMap& line_indexes_map) const; + + std::string NodeToLineIndex(id, const LineIndexesMap&) const; + gfx::NativeViewAccessible LineIndexToNode( + const base::string16 line_index, + const LineIndexesMap& line_indexes_map) const; + base::string16 ProcessTreeForOutput( const base::DictionaryValue& node, base::DictionaryValue* filtered_dict_result = nullptr) override; + + std::string FormatAttributeValue(const base::Value& value); }; // static @@ -228,9 +210,14 @@ AccessibilityTreeFormatterMac::BuildAccessibilityTree( BrowserAccessibility* root) { DCHECK(root); - std::unique_ptr<base::DictionaryValue> dict(new base::DictionaryValue); BrowserAccessibilityCocoa* cocoa_root = ToBrowserAccessibilityCocoa(root); - RecursiveBuildAccessibilityTree(cocoa_root, dict.get()); + + int counter = 0; + LineIndexesMap line_indexes_map; + RecursiveBuildLineIndexesMap(cocoa_root, &line_indexes_map, &counter); + + std::unique_ptr<base::DictionaryValue> dict(new base::DictionaryValue); + RecursiveBuildAccessibilityTree(cocoa_root, line_indexes_map, dict.get()); return dict; } @@ -257,36 +244,397 @@ AccessibilityTreeFormatterMac::BuildAccessibilityTreeForPattern( void AccessibilityTreeFormatterMac::RecursiveBuildAccessibilityTree( const BrowserAccessibilityCocoa* cocoa_node, + const LineIndexesMap& line_indexes_map, base::DictionaryValue* dict) { - AddProperties(cocoa_node, dict); + AddProperties(cocoa_node, line_indexes_map, dict); auto children = std::make_unique<base::ListValue>(); for (BrowserAccessibilityCocoa* cocoa_child in [cocoa_node children]) { std::unique_ptr<base::DictionaryValue> child_dict( new base::DictionaryValue); - RecursiveBuildAccessibilityTree(cocoa_child, child_dict.get()); + RecursiveBuildAccessibilityTree(cocoa_child, line_indexes_map, + child_dict.get()); children->Append(std::move(child_dict)); } dict->Set(kChildrenDictAttr, std::move(children)); } +void AccessibilityTreeFormatterMac::RecursiveBuildLineIndexesMap( + const BrowserAccessibilityCocoa* cocoa_node, + LineIndexesMap* line_indexes_map, + int* counter) { + const base::string16 line_index = + base::string16(1, ':') + base::NumberToString16(++(*counter)); + line_indexes_map->insert({cocoa_node, line_index}); + for (BrowserAccessibilityCocoa* cocoa_child in [cocoa_node children]) { + RecursiveBuildLineIndexesMap(cocoa_child, line_indexes_map, counter); + } +} + void AccessibilityTreeFormatterMac::AddProperties( const BrowserAccessibilityCocoa* cocoa_node, - base::DictionaryValue* dict) { + const LineIndexesMap& line_indexes_map, + base::Value* dict) { + // DOM element id BrowserAccessibility* node = [cocoa_node owner]; - dict->SetString("id", base::NumberToString16(node->GetId())); + dict->SetKey("id", base::Value(base::NumberToString16(node->GetId()))); + + base::string16 line_index = base::ASCIIToUTF16("-1"); + if (line_indexes_map.find(cocoa_node) != line_indexes_map.end()) { + line_index = line_indexes_map.at(cocoa_node); + } + // Attributes for (NSString* supportedAttribute in [cocoa_node accessibilityAttributeNames]) { - if (FilterPropertyName(SysNSStringToUTF16(supportedAttribute))) { + if (GetMatchingPropertyNode(line_index, + SysNSStringToUTF16(supportedAttribute))) { id value = [cocoa_node accessibilityAttributeValue:supportedAttribute]; if (value != nil) { - dict->Set(SysNSStringToUTF8(supportedAttribute), PopulateObject(value)); + dict->SetPath(SysNSStringToUTF8(supportedAttribute), + PopulateObject(value, line_indexes_map)); } } } - dict->Set(kPositionDictAttr, PopulatePosition(*node)); - dict->Set(kSizeDictAttr, PopulateSize(cocoa_node)); + + // Parameterized attributes + for (NSString* supportedAttribute in + [cocoa_node accessibilityParameterizedAttributeNames]) { + auto propnode = GetMatchingPropertyNode( + line_index, SysNSStringToUTF16(supportedAttribute)); + IdOrError param = ParamByPropertyNode(propnode, line_indexes_map); + if (param.IsError()) { + dict->SetPath(base::UTF16ToUTF8(propnode.original_property), + base::Value(kFailedToParseArgsError)); + continue; + } + + if (param.IsNotNil()) { + id value = [cocoa_node accessibilityAttributeValue:supportedAttribute + forParameter:*param]; + dict->SetPath(base::UTF16ToUTF8(propnode.original_property), + PopulateObject(value, line_indexes_map)); + } + } + + // Position and size + dict->SetPath(kPositionDictAttr, PopulatePosition(cocoa_node)); + dict->SetPath(kSizeDictAttr, PopulateSize(cocoa_node)); +} + +AccessibilityTreeFormatterMac::IdOrError +AccessibilityTreeFormatterMac::ParamByPropertyNode( + const PropertyNode& property_node, + const LineIndexesMap& line_indexes_map) const { + IdOrError param; + std::string property_name = base::UTF16ToASCII(property_node.name_or_value); + + if (property_name == "AXLineForIndex") { // Int + param = PropertyNodeToInt(property_node); + } else if (property_name == "AXCellForColumnAndRow") { // IntArray + param = PropertyNodeToIntArray(property_node); + } else if (property_name == "AXStringForRange") { // NSRange + param = PropertyNodeToRange(property_node); + } else if (property_name == "AXIndexForChildUIElement") { // UIElement + param = PropertyNodeToUIElement(property_node, line_indexes_map); + } else if (property_name == "AXIndexForTextMarker") { // TextMarker + param = PropertyNodeToTextMarker(property_node, line_indexes_map); + } + + return param; +} + +// NSNumber. Format: integer. +NSNumber* AccessibilityTreeFormatterMac::PropertyNodeToInt( + const PropertyNode& propnode) const { + if (propnode.parameters.size() != 1) { + INT_FAIL(propnode, "single argument is expected") + } + + const auto& intnode = propnode.parameters[0]; + base::Optional<int> param = intnode.AsInt(); + if (!param) { + INT_FAIL(propnode, "not a number") + } + return [NSNumber numberWithInt:*param]; +} + +// NSArray of two NSNumber. Format: [integer, integer]. +NSArray* AccessibilityTreeFormatterMac::PropertyNodeToIntArray( + const PropertyNode& propnode) const { + if (propnode.parameters.size() != 1) { + INTARRAY_FAIL(propnode, "single argument is expected") + } + + const auto& arraynode = propnode.parameters[0]; + if (arraynode.name_or_value != base::ASCIIToUTF16("[]")) { + INTARRAY_FAIL(propnode, "not array") + } + + NSMutableArray* array = + [[NSMutableArray alloc] initWithCapacity:arraynode.parameters.size()]; + for (const auto& paramnode : arraynode.parameters) { + base::Optional<int> param = paramnode.AsInt(); + if (!param) { + INTARRAY_FAIL(propnode, paramnode.name_or_value + + base::UTF8ToUTF16(" is not a number")) + } + [array addObject:@(*param)]; + } + return array; +} + +// NSRange. Format: {loc: integer, len: integer}. +NSValue* AccessibilityTreeFormatterMac::PropertyNodeToRange( + const PropertyNode& propnode) const { + if (propnode.parameters.size() != 1) { + NSRANGE_FAIL(propnode, "single argument is expected") + } + + const auto& dictnode = propnode.parameters[0]; + if (!dictnode.IsDict()) { + NSRANGE_FAIL(propnode, "dictionary is expected") + } + + base::Optional<int> loc = dictnode.FindIntKey("loc"); + if (!loc) { + NSRANGE_FAIL(propnode, "no loc or loc is not a number") + } + + base::Optional<int> len = dictnode.FindIntKey("len"); + if (!len) { + NSRANGE_FAIL(propnode, "no len or len is not a number") + } + + return [NSValue valueWithRange:NSMakeRange(*loc, *len)]; +} + +// UIElement. Format: :line_num. +gfx::NativeViewAccessible +AccessibilityTreeFormatterMac::PropertyNodeToUIElement( + const PropertyNode& propnode, + const LineIndexesMap& line_indexes_map) const { + if (propnode.parameters.size() != 1) { + UIELEMENT_FAIL(propnode, "single argument is expected") + } + + gfx::NativeViewAccessible uielement = + LineIndexToNode(propnode.parameters[0].name_or_value, line_indexes_map); + if (!uielement) { + UIELEMENT_FAIL(propnode, "no corresponding UIElement was found in the tree") + } + return uielement; +} + +id AccessibilityTreeFormatterMac::PropertyNodeToTextMarker( + const PropertyNode& propnode, + const LineIndexesMap& line_indexes_map) const { + if (propnode.parameters.size() != 1) { + TEXTMARKER_FAIL(propnode, "single argument is expected") + } + + const auto& tmnode = propnode.parameters[0]; + if (!tmnode.IsDict()) { + TEXTMARKER_FAIL(propnode, "dictionary is expected") + } + if (tmnode.parameters.size() != 3) { + TEXTMARKER_FAIL(propnode, "wrong number of dictionary elements") + } + + BrowserAccessibilityCocoa* anchor_cocoa = + LineIndexToNode(tmnode.parameters[0].name_or_value, line_indexes_map); + if (!anchor_cocoa) { + TEXTMARKER_FAIL(propnode, "1st argument: wrong anchor") + } + + base::Optional<int> offset = tmnode.parameters[1].AsInt(); + if (!offset) { + TEXTMARKER_FAIL(propnode, "2nd argument: wrong offset") + } + + ax::mojom::TextAffinity affinity; + const base::string16& affinity_str = tmnode.parameters[2].name_or_value; + if (affinity_str == base::UTF8ToUTF16("none")) { + affinity = ax::mojom::TextAffinity::kNone; + } else if (affinity_str == base::UTF8ToUTF16("down")) { + affinity = ax::mojom::TextAffinity::kDownstream; + } else if (affinity_str == base::UTF8ToUTF16("up")) { + affinity = ax::mojom::TextAffinity::kUpstream; + } else { + TEXTMARKER_FAIL(propnode, "3rd argument: wrong affinity") + } + + return content::AXTextMarkerFrom(anchor_cocoa, *offset, affinity); +} + +base::Value AccessibilityTreeFormatterMac::PopulateSize( + const BrowserAccessibilityCocoa* cocoa_node) const { + base::Value size(base::Value::Type::DICTIONARY); + NSSize node_size = [[cocoa_node size] sizeValue]; + size.SetIntPath(kHeightDictAttr, static_cast<int>(node_size.height)); + size.SetIntPath(kWidthDictAttr, static_cast<int>(node_size.width)); + return size; +} + +base::Value AccessibilityTreeFormatterMac::PopulatePosition( + const BrowserAccessibilityCocoa* cocoa_node) const { + BrowserAccessibility* node = [cocoa_node owner]; + BrowserAccessibilityManager* root_manager = node->manager()->GetRootManager(); + DCHECK(root_manager); + + // The NSAccessibility position of an object is in global coordinates and + // based on the lower-left corner of the object. To make this easier and less + // confusing, convert it to local window coordinates using the top-left + // corner when dumping the position. + BrowserAccessibility* root = root_manager->GetRoot(); + BrowserAccessibilityCocoa* cocoa_root = ToBrowserAccessibilityCocoa(root); + NSPoint root_position = [[cocoa_root position] pointValue]; + NSSize root_size = [[cocoa_root size] sizeValue]; + int root_top = -static_cast<int>(root_position.y + root_size.height); + int root_left = static_cast<int>(root_position.x); + + NSPoint node_position = [[cocoa_node position] pointValue]; + NSSize node_size = [[cocoa_node size] sizeValue]; + + base::Value position(base::Value::Type::DICTIONARY); + position.SetIntPath(kXCoordDictAttr, + static_cast<int>(node_position.x - root_left)); + position.SetIntPath( + kYCoordDictAttr, + static_cast<int>(-node_position.y - node_size.height - root_top)); + return position; +} + +base::Value AccessibilityTreeFormatterMac::PopulateObject( + id value, + const LineIndexesMap& line_indexes_map) const { + if (value == nil) { + return base::Value(kNULLValue); + } + + // NSArray + if ([value isKindOfClass:[NSArray class]]) { + return PopulateArray((NSArray*)value, line_indexes_map); + } + + // NSNumber + if ([value isKindOfClass:[NSNumber class]]) { + return base::Value([value intValue]); + } + + // NSRange + if ([value isKindOfClass:[NSValue class]] && + 0 == strcmp([value objCType], @encode(NSRange))) { + return PopulateRange([value rangeValue]); + } + + // AXTextMarker + if (content::IsAXTextMarker(value)) { + return PopulateTextPosition(content::AXTextMarkerToPosition(value).get(), + line_indexes_map); + } + + // AXTextMarkerRange + if (content::IsAXTextMarkerRange(value)) { + return PopulateTextMarkerRange(value, line_indexes_map); + } + + // Accessible object + if ([value isKindOfClass:[BrowserAccessibilityCocoa class]]) { + return base::Value(NodeToLineIndex(value, line_indexes_map)); + } + + // Scalar value. + return base::Value( + SysNSStringToUTF16([NSString stringWithFormat:@"%@", value])); +} + +base::Value AccessibilityTreeFormatterMac::PopulateRange( + NSRange node_range) const { + base::Value range(base::Value::Type::DICTIONARY); + range.SetIntPath(kRangeLocDictAttr, static_cast<int>(node_range.location)); + range.SetIntPath(kRangeLenDictAttr, static_cast<int>(node_range.length)); + return range; +} + +base::Value AccessibilityTreeFormatterMac::PopulateTextPosition( + BrowserAccessibilityPosition::AXPositionInstance::pointer position, + const LineIndexesMap& line_indexes_map) const { + if (position->IsNullPosition()) { + return base::Value(kNULLValue); + } + + BrowserAccessibility* anchor = position->GetAnchor(); + BrowserAccessibilityCocoa* cocoa_anchor = ToBrowserAccessibilityCocoa(anchor); + + std::string affinity; + switch (position->affinity()) { + case ax::mojom::TextAffinity::kNone: + affinity = "none"; + break; + case ax::mojom::TextAffinity::kDownstream: + affinity = "down"; + break; + case ax::mojom::TextAffinity::kUpstream: + affinity = "up"; + break; + } + + base::Value set(base::Value::Type::DICTIONARY); + const std::string setkey_prefix = kSetKeyPrefixDictAttr; + set.SetStringPath(setkey_prefix + "index1_anchor", + NodeToLineIndex(cocoa_anchor, line_indexes_map)); + set.SetIntPath(setkey_prefix + "index2_offset", position->text_offset()); + set.SetStringPath(setkey_prefix + "index3_affinity", + kConstValuePrefix + affinity); + return set; +} + +base::Value AccessibilityTreeFormatterMac::PopulateTextMarkerRange( + id object, + const LineIndexesMap& line_indexes_map) const { + auto range = content::AXTextMarkerRangeToRange(object); + if (range.IsNull()) { + return base::Value(kNULLValue); + } + + base::Value dict(base::Value::Type::DICTIONARY); + dict.SetPath("anchor", + PopulateTextPosition(range.anchor(), line_indexes_map)); + dict.SetPath("focus", PopulateTextPosition(range.focus(), line_indexes_map)); + return dict; +} + +base::Value AccessibilityTreeFormatterMac::PopulateArray( + NSArray* node_array, + const LineIndexesMap& line_indexes_map) const { + base::Value list(base::Value::Type::LIST); + for (NSUInteger i = 0; i < [node_array count]; i++) + list.Append(PopulateObject([node_array objectAtIndex:i], line_indexes_map)); + return list; +} + +std::string AccessibilityTreeFormatterMac::NodeToLineIndex( + id cocoa_node, + const LineIndexesMap& line_indexes_map) const { + std::string line_index = ":unknown"; + auto index_iterator = line_indexes_map.find(cocoa_node); + if (index_iterator != line_indexes_map.end()) { + line_index = base::UTF16ToUTF8(index_iterator->second); + } + return kConstValuePrefix + line_index; +} + +gfx::NativeViewAccessible AccessibilityTreeFormatterMac::LineIndexToNode( + const base::string16 line_index, + const LineIndexesMap& line_indexes_map) const { + for (std::pair<const gfx::NativeViewAccessible, base::string16> item : + line_indexes_map) { + if (item.second == line_index) { + return item.first; + } + } + return nil; } base::string16 AccessibilityTreeFormatterMac::ProcessTreeForOutput( @@ -314,47 +662,94 @@ base::string16 AccessibilityTreeFormatterMac::ProcessTreeForOutput( // Expose all other attributes. for (auto item : dict.DictItems()) { - if (item.second.is_string()) { - if (item.first != role_attr && item.first != subrole_attr) { - WriteAttribute(false, - StringPrintf("%s='%s'", item.first.c_str(), - item.second.GetString().c_str()), - &line); - } + if (item.second.is_string() && + (item.first == role_attr || item.first == subrole_attr)) { continue; } // Special processing for position and size. - if (item.second.is_dict()) { - if (item.first == kPositionDictAttr) { - WriteAttribute(false, - FormatCoordinates( - base::Value::AsDictionaryValue(item.second), - kPositionDictAttr, kXCoordDictAttr, kYCoordDictAttr), - &line); - continue; - } - if (item.first == kSizeDictAttr) { - WriteAttribute( - false, - FormatCoordinates(base::Value::AsDictionaryValue(item.second), - kSizeDictAttr, kWidthDictAttr, kHeightDictAttr), - &line); - continue; - } + if (item.first == kPositionDictAttr) { + WriteAttribute(false, + FormatCoordinates( + base::Value::AsDictionaryValue(item.second), + kPositionDictAttr, kXCoordDictAttr, kYCoordDictAttr), + &line); + continue; + } + if (item.first == kSizeDictAttr) { + WriteAttribute( + false, + FormatCoordinates(base::Value::AsDictionaryValue(item.second), + kSizeDictAttr, kWidthDictAttr, kHeightDictAttr), + &line); + continue; } - // Write everything else as JSON. - std::string json_value; - base::JSONWriter::Write(item.second, &json_value); + // Write formatted value. + std::string formatted_value = FormatAttributeValue(item.second); WriteAttribute( - false, StringPrintf("%s=%s", item.first.c_str(), json_value.c_str()), + false, + StringPrintf("%s=%s", item.first.c_str(), formatted_value.c_str()), &line); } return line; } +std::string AccessibilityTreeFormatterMac::FormatAttributeValue( + const base::Value& value) { + // String. + if (value.is_string()) { + // Special handling for constants which are exposed as is, i.e. with no + // quotation marks. + std::string const_prefix = kConstValuePrefix; + if (base::StartsWith(value.GetString(), const_prefix, + base::CompareCase::SENSITIVE)) { + return value.GetString().substr(const_prefix.length()); + } + return "'" + value.GetString() + "'"; + } + + // Integer. + if (value.is_int()) { + return base::NumberToString(value.GetInt()); + } + + // List: exposed as [value1, ..., valueN]; + if (value.is_list()) { + std::string output; + for (const auto& item : value.GetList()) { + if (!output.empty()) { + output += ", "; + } + output += FormatAttributeValue(item); + } + return "[" + output + "]"; + } + + // Dictionary. Exposed as {key1: value1, ..., keyN: valueN}. Set-like + // dictionary is exposed as {value1, ..., valueN}. + if (value.is_dict()) { + const std::string setkey_prefix(kSetKeyPrefixDictAttr); + std::string output; + for (const auto& item : value.DictItems()) { + if (!output.empty()) { + output += ", "; + } + // Special set-like dictionaries handling: keys are prefixed by + // "_setkey_". + if (base::StartsWith(item.first, setkey_prefix, + base::CompareCase::SENSITIVE)) { + output += FormatAttributeValue(item.second); + } else { + output += item.first + ": " + FormatAttributeValue(item.second); + } + } + return "{" + output + "}"; + } + return ""; +} + base::FilePath::StringType AccessibilityTreeFormatterMac::GetExpectedFileSuffix() { return FILE_PATH_LITERAL("-expected-mac.txt"); diff --git a/chromium/content/browser/accessibility/accessibility_tree_formatter_mac_browsertest.mm b/chromium/content/browser/accessibility/accessibility_tree_formatter_mac_browsertest.mm new file mode 100644 index 00000000000..5f6db82d33d --- /dev/null +++ b/chromium/content/browser/accessibility/accessibility_tree_formatter_mac_browsertest.mm @@ -0,0 +1,293 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/browser/accessibility/accessibility_tree_formatter_base.h" +#include "content/browser/accessibility/browser_accessibility.h" +#include "content/browser/accessibility/browser_accessibility_manager.h" +#include "content/browser/web_contents/web_contents_impl.h" +#include "content/public/test/accessibility_notification_waiter.h" +#include "content/public/test/browser_test.h" +#include "content/public/test/browser_test_utils.h" +#include "content/public/test/content_browser_test.h" +#include "content/public/test/content_browser_test_utils.h" +#include "content/public/test/test_utils.h" +#include "content/shell/browser/shell.h" +#include "net/base/data_url.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/gtest_mac.h" +#include "url/gurl.h" + +namespace content { + +namespace { + +class AccessibilityTreeFormatterMacBrowserTest : public ContentBrowserTest { + public: + AccessibilityTreeFormatterMacBrowserTest() {} + ~AccessibilityTreeFormatterMacBrowserTest() override {} + + // Checks the formatted accessible tree for the given data URL. + void TestAndCheck(const char* url, + const std::vector<const char*>& filters, + const char* expected) const; + + // Tests wrong parameters for an attribute in a single run + void TestWrongParameters(const char* url, + const std::vector<const char*>& parameters, + const char* filter_pattern, + const char* expected_pattern) const; + + protected: + BrowserAccessibilityManager* GetManager() const { + WebContentsImpl* web_contents = + static_cast<WebContentsImpl*>(shell()->web_contents()); + return web_contents->GetRootBrowserAccessibilityManager(); + } +}; + +void AccessibilityTreeFormatterMacBrowserTest::TestAndCheck( + const char* url, + const std::vector<const char*>& filters, + const char* expected) const { + EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL))); + + AccessibilityNotificationWaiter waiter(shell()->web_contents(), + ui::kAXModeComplete, + ax::mojom::Event::kLoadComplete); + + EXPECT_TRUE(NavigateToURL(shell(), GURL(url))); + waiter.WaitForNotification(); + + // Set property filters + std::unique_ptr<AccessibilityTreeFormatter> formatter = + AccessibilityTreeFormatter::Create(); + + std::vector<AccessibilityTreeFormatter::PropertyFilter> property_filters; + + for (const char* filter : filters) { + property_filters.push_back(AccessibilityTreeFormatter::PropertyFilter( + base::UTF8ToUTF16(filter), + AccessibilityTreeFormatter::PropertyFilter::ALLOW_EMPTY)); + } + + formatter->AddDefaultFilters(&property_filters); + formatter->SetPropertyFilters(property_filters); + + // Format the tree + BrowserAccessibility* root = GetManager()->GetRoot(); + CHECK(root); + + base::string16 contents; + formatter->FormatAccessibilityTreeForTesting(root, &contents); + + auto got = base::UTF16ToUTF8(contents); + EXPECT_EQ(got, expected); +} + +void AccessibilityTreeFormatterMacBrowserTest::TestWrongParameters( + const char* url, + const std::vector<const char*>& parameters, + const char* filter_pattern, + const char* expected_pattern) const { + std::string placeholder("Argument"); + size_t expected_pos = std::string(expected_pattern).find(placeholder); + ASSERT_FALSE(expected_pos == std::string::npos); + + size_t filter_pos = std::string(filter_pattern).find(placeholder); + ASSERT_FALSE(filter_pos == std::string::npos); + + for (const char* parameter : parameters) { + std::string expected(expected_pattern); + expected.replace(expected_pos, placeholder.length(), parameter); + + std::string filter(filter_pattern); + filter.replace(filter_pos, placeholder.length(), parameter); + + TestAndCheck(url, {filter.c_str()}, expected.c_str()); + } +} + +} // namespace + +IN_PROC_BROWSER_TEST_F(AccessibilityTreeFormatterMacBrowserTest, + DefaultAttributes) { + TestAndCheck(R"~~(data:text/html, + <input aria-label='input'>)~~", + {}, + R"~~(AXWebArea +++AXGroup +++++AXTextField AXDescription='input' +)~~"); +} + +IN_PROC_BROWSER_TEST_F(AccessibilityTreeFormatterMacBrowserTest, + LineIndexFilter) { + TestAndCheck(R"~~(data:text/html, + <input class='input_at_3rd_line'> + <input class='input_at_4th_line'> + <input class='input_at_5th_line'>)~~", + {":3,:5;AXDOMClassList=*"}, R"~~(AXWebArea +++AXGroup +++++AXTextField AXDOMClassList=['input_at_3rd_line'] +++++AXTextField +++++AXTextField AXDOMClassList=['input_at_5th_line'] +)~~"); +} + +IN_PROC_BROWSER_TEST_F(AccessibilityTreeFormatterMacBrowserTest, + Serialize_AXTextMarker) { + TestAndCheck(R"~~(data:text/html, + <p>Paragraph</p>)~~", + {":3;AXStartTextMarker=*"}, R"~~(AXWebArea +++AXGroup +++++AXStaticText AXStartTextMarker={:1, 0, down} AXValue='Paragraph' +)~~"); +} + +IN_PROC_BROWSER_TEST_F(AccessibilityTreeFormatterMacBrowserTest, + Serialize_AXTextMarkerRange) { + TestAndCheck(R"~~(data:text/html, + <p id='p'>Paragraph</p> + <script> + window.getSelection().selectAllChildren(document.getElementById('p')); + </script>)~~", + {":3;AXSelectedTextMarkerRange=*"}, R"~~(AXWebArea +++AXGroup +++++AXStaticText AXSelectedTextMarkerRange={anchor: {:3, 0, down}, focus: {:2, -1, down}} AXValue='Paragraph' +)~~"); +} + +IN_PROC_BROWSER_TEST_F(AccessibilityTreeFormatterMacBrowserTest, + ParameterizedAttributes_Int) { + TestAndCheck(R"~~(data:text/html, + <p contentEditable='true'>Text</p>)~~", + {":2;AXLineForIndex(0)=*"}, R"~~(AXWebArea +++AXTextArea AXLineForIndex(0)=0 AXValue='Text' +++++AXStaticText AXValue='Text' +)~~"); +} + +IN_PROC_BROWSER_TEST_F(AccessibilityTreeFormatterMacBrowserTest, + ParameterizedAttributes_Int_WrongParameters) { + TestWrongParameters(R"~~(data:text/html, + <p contentEditable='true'>Text</p>)~~", + {"1, 2", "NaN"}, ":2;AXLineForIndex(Argument)=*", + R"~~(AXWebArea +++AXTextArea AXLineForIndex(Argument)=ERROR:FAILED_TO_PARSE_ARGS AXValue='Text' +++++AXStaticText AXValue='Text' +)~~"); +} + +IN_PROC_BROWSER_TEST_F(AccessibilityTreeFormatterMacBrowserTest, + ParameterizedAttributes_IntArray) { + TestAndCheck(R"~~(data:text/html, + <table role="grid"><tr><td>CELL</td></tr></table>)~~", + {"AXCellForColumnAndRow([0, 0])=*"}, R"~~(AXWebArea +++AXTable AXCellForColumnAndRow([0, 0])=:4 +++++AXRow +++++++AXCell +++++++++AXStaticText AXValue='CELL' +++++AXColumn +++++++AXCell +++++++++AXStaticText AXValue='CELL' +++++AXGroup +)~~"); +} + +IN_PROC_BROWSER_TEST_F(AccessibilityTreeFormatterMacBrowserTest, + ParameterizedAttributes_IntArray_NilValue) { + TestAndCheck(R"~~(data:text/html, + <table role="grid"></table>)~~", + {"AXCellForColumnAndRow([0, 0])=*"}, R"~~(AXWebArea +++AXTable AXCellForColumnAndRow([0, 0])=NULL +++++AXGroup +)~~"); +} + +IN_PROC_BROWSER_TEST_F(AccessibilityTreeFormatterMacBrowserTest, + ParameterizedAttributes_IntArray_WrongParameters) { + TestWrongParameters(R"~~(data:text/html, + <table role="grid"><tr><td>CELL</td></tr></table>)~~", + {"0, 0", "{1, 2}", "[1, NaN]", "[NaN, 1]"}, + "AXCellForColumnAndRow(Argument)=*", R"~~(AXWebArea +++AXTable AXCellForColumnAndRow(Argument)=ERROR:FAILED_TO_PARSE_ARGS +++++AXRow +++++++AXCell +++++++++AXStaticText AXValue='CELL' +++++AXColumn +++++++AXCell +++++++++AXStaticText AXValue='CELL' +++++AXGroup +)~~"); +} + +IN_PROC_BROWSER_TEST_F(AccessibilityTreeFormatterMacBrowserTest, + ParameterizedAttributes_NSRange) { + TestAndCheck(R"~~(data:text/html, + <p contentEditable='true'>Text</p>)~~", + {":2;AXStringForRange({loc: 1, len: 2})=*"}, R"~~(AXWebArea +++AXTextArea AXStringForRange({loc: 1, len: 2})='ex' AXValue='Text' +++++AXStaticText AXValue='Text' +)~~"); +} + +IN_PROC_BROWSER_TEST_F(AccessibilityTreeFormatterMacBrowserTest, + ParameterizedAttributes_NSRange_WrongParameters) { + TestWrongParameters(R"~~(data:text/html, + <p contentEditable='true'>Text</p>)~~", + {"1, 2", "[]", "{loc: 1, leno: 2}", "{loco: 1, len: 2}", + "{loc: NaN, len: 2}", "{loc: 2, len: NaN}"}, + ":2;AXStringForRange(Argument)=*", R"~~(AXWebArea +++AXTextArea AXStringForRange(Argument)=ERROR:FAILED_TO_PARSE_ARGS AXValue='Text' +++++AXStaticText AXValue='Text' +)~~"); +} + +IN_PROC_BROWSER_TEST_F(AccessibilityTreeFormatterMacBrowserTest, + ParameterizedAttributes_UIElement) { + TestAndCheck(R"~~(data:text/html, + <p contentEditable='true'>Text</p>)~~", + {":2;AXIndexForChildUIElement(:3)=*"}, R"~~(AXWebArea +++AXTextArea AXIndexForChildUIElement(:3)=0 AXValue='Text' +++++AXStaticText AXValue='Text' +)~~"); +} + +IN_PROC_BROWSER_TEST_F(AccessibilityTreeFormatterMacBrowserTest, + ParameterizedAttributes_UIElement_WrongParameters) { + TestWrongParameters(R"~~(data:text/html, + <p contentEditable='true'>Text</p>)~~", + {"1, 2", "2", ":4"}, + ":2;AXIndexForChildUIElement(Argument)=*", + R"~~(AXWebArea +++AXTextArea AXIndexForChildUIElement(Argument)=ERROR:FAILED_TO_PARSE_ARGS AXValue='Text' +++++AXStaticText AXValue='Text' +)~~"); +} + +IN_PROC_BROWSER_TEST_F(AccessibilityTreeFormatterMacBrowserTest, + ParameterizedAttributes_TextMarker) { + TestAndCheck(R"~~(data:text/html, + <p>Text</p>)~~", + {":1;AXIndexForTextMarker({:2, 1, down})=*"}, + R"~~(AXWebArea AXIndexForTextMarker({:2, 1, down})=1 +++AXGroup +++++AXStaticText AXValue='Text' +)~~"); +} + +IN_PROC_BROWSER_TEST_F(AccessibilityTreeFormatterMacBrowserTest, + ParameterizedAttributes_TextMarker_WrongParameters) { + TestWrongParameters( + R"~~(data:text/html, + <p>Text</p>)~~", + {"1, 2", "2", "{2, 1, down}", "{:2, NaN, down}", "{:2, 1, hoho}"}, + ":1;AXIndexForTextMarker(Argument)=*", + R"~~(AXWebArea AXIndexForTextMarker(Argument)=ERROR:FAILED_TO_PARSE_ARGS +++AXGroup +++++AXStaticText AXValue='Text' +)~~"); +} + +} // namespace content diff --git a/chromium/content/browser/accessibility/accessibility_win_browsertest.cc b/chromium/content/browser/accessibility/accessibility_win_browsertest.cc index bda863e74b4..1c33a05fbef 100644 --- a/chromium/content/browser/accessibility/accessibility_win_browsertest.cc +++ b/chromium/content/browser/accessibility/accessibility_win_browsertest.cc @@ -52,6 +52,7 @@ #include "ui/accessibility/accessibility_switches.h" #include "ui/accessibility/ax_event_generator.h" #include "ui/accessibility/platform/ax_fragment_root_win.h" +#include "ui/accessibility/platform/uia_registrar_win.h" #include "ui/aura/window.h" #include "ui/aura/window_tree_host.h" @@ -457,7 +458,7 @@ BrowserAccessibility* AccessibilityWinBrowserTest::FindNode( BrowserAccessibilityManager* AccessibilityWinBrowserTest::GetManager() { WebContentsImpl* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents()); - return web_contents->GetRootBrowserAccessibilityManager(); + return web_contents->GetOrCreateRootBrowserAccessibilityManager(); } // Retrieve the accessibility node in the subtree that matches the accessibility @@ -882,10 +883,12 @@ class WebContentsUIAParentNavigationInDestroyedWatcher private: // Overridden WebContentsObserver methods. void WebContentsDestroyed() override { - // Test navigating to the parent node via UIA + // Test navigating to the parent node via UIA. Microsoft::WRL::ComPtr<IUIAutomationElement> parent; tree_walker_->GetParentElement(root_.Get(), &parent); - CHECK(parent.Get() == nullptr); + + // The original bug resulted in a crash when making this call. + parent.Get(); run_loop_.Quit(); } @@ -2633,13 +2636,13 @@ IN_PROC_BROWSER_TEST_F(AccessibilityWinBrowserTest, TestSetSelection) { hr = input_text->get_selection(0, &start_offset, &end_offset); EXPECT_EQ(S_OK, hr); - // Start and end offsets are always swapped to be in ascending order. + // Start and end offsets should always be swapped to be in ascending order + // according to the IA2 Spec. EXPECT_EQ(1, start_offset); EXPECT_EQ(contents_string_length, end_offset); } -IN_PROC_BROWSER_TEST_F(AccessibilityWinBrowserTest, - DISABLED_TestSetSelectionRanges) { +IN_PROC_BROWSER_TEST_F(AccessibilityWinBrowserTest, TestSetSelectionRanges) { Microsoft::WRL::ComPtr<IAccessibleText> input_text; SetUpInputField(&input_text); Microsoft::WRL::ComPtr<IAccessible2_4> ax_input; @@ -2672,11 +2675,15 @@ IN_PROC_BROWSER_TEST_F(AccessibilityWinBrowserTest, EXPECT_EQ(S_OK, hr); EXPECT_EQ(1, n_ranges); ASSERT_NE(nullptr, ranges); + ASSERT_NE(nullptr, ranges[0].anchor); EXPECT_EQ(ax_input.Get(), ranges[0].anchor); EXPECT_EQ(0, ranges[0].anchorOffset); + ASSERT_NE(nullptr, ranges[0].active); EXPECT_EQ(ax_input.Get(), ranges[0].active); EXPECT_EQ(contents_string_length, ranges[0].activeOffset); + ranges[0].anchor->Release(); + ranges[0].active->Release(); n_ranges = 1; ranges = reinterpret_cast<IA2Range*>(CoTaskMemRealloc(ranges, sizeof(IA2Range))); @@ -2690,14 +2697,22 @@ IN_PROC_BROWSER_TEST_F(AccessibilityWinBrowserTest, ranges = nullptr; n_ranges = 0; + // For native plain text fields, e.g. input and textarea, anchor and active + // offsets are always swapped to be in ascending order by the renderer. The + // selection's directionality is lost. hr = ax_input->get_selectionRanges(&ranges, &n_ranges); EXPECT_EQ(S_OK, hr); EXPECT_EQ(1, n_ranges); ASSERT_NE(nullptr, ranges); + ASSERT_NE(nullptr, ranges[0].anchor); EXPECT_EQ(ax_input.Get(), ranges[0].anchor); - EXPECT_EQ(contents_string_length, ranges[0].anchorOffset); + EXPECT_EQ(1, ranges[0].anchorOffset); + ASSERT_NE(nullptr, ranges[0].active); EXPECT_EQ(ax_input.Get(), ranges[0].active); - EXPECT_EQ(1, ranges[0].activeOffset); + EXPECT_EQ(contents_string_length, ranges[0].activeOffset); + + ranges[0].anchor->Release(); + ranges[0].active->Release(); CoTaskMemFree(ranges); ranges = nullptr; } @@ -2744,7 +2759,7 @@ IN_PROC_BROWSER_TEST_F(AccessibilityWinBrowserTest, TestMultiLineSetSelection) { } IN_PROC_BROWSER_TEST_F(AccessibilityWinBrowserTest, - DISABLED_TestMultiLineSetSelectionRanges) { + TestMultiLineSetSelectionRanges) { Microsoft::WRL::ComPtr<IAccessibleText> textarea_text; SetUpTextareaField(&textarea_text); Microsoft::WRL::ComPtr<IAccessible2_4> ax_textarea; @@ -2777,32 +2792,46 @@ IN_PROC_BROWSER_TEST_F(AccessibilityWinBrowserTest, EXPECT_EQ(S_OK, hr); EXPECT_EQ(1, n_ranges); ASSERT_NE(nullptr, ranges); + ASSERT_NE(nullptr, ranges[0].anchor); EXPECT_EQ(ax_textarea.Get(), ranges[0].anchor); EXPECT_EQ(0, ranges[0].anchorOffset); + ASSERT_NE(nullptr, ranges[0].active); EXPECT_EQ(ax_textarea.Get(), ranges[0].active); EXPECT_EQ(contents_string_length, ranges[0].activeOffset); + ranges[0].anchor->Release(); + ranges[0].active->Release(); n_ranges = 1; ranges = reinterpret_cast<IA2Range*>(CoTaskMemRealloc(ranges, sizeof(IA2Range))); + ranges[0].anchor = ax_textarea.Get(); ranges[0].anchorOffset = contents_string_length - 1; ranges[0].active = ax_textarea.Get(); ranges[0].activeOffset = 0; EXPECT_HRESULT_SUCCEEDED(ax_textarea->setSelectionRanges(n_ranges, ranges)); waiter.WaitForNotification(); + CoTaskMemFree(ranges); ranges = nullptr; n_ranges = 0; + // For native plain text fields, e.g. input and textarea, anchor and active + // offsets are always swapped to be in ascending order by the renderer. The + // selection's directionality is lost. hr = ax_textarea->get_selectionRanges(&ranges, &n_ranges); EXPECT_EQ(S_OK, hr); EXPECT_EQ(1, n_ranges); ASSERT_NE(nullptr, ranges); + ASSERT_NE(nullptr, ranges[0].anchor); EXPECT_EQ(ax_textarea.Get(), ranges[0].anchor); - EXPECT_EQ(contents_string_length - 1, ranges[0].anchorOffset); + EXPECT_EQ(0, ranges[0].anchorOffset); + ASSERT_NE(nullptr, ranges[0].active); EXPECT_EQ(ax_textarea.Get(), ranges[0].active); - EXPECT_EQ(0, ranges[0].activeOffset); + EXPECT_EQ(contents_string_length - 1, ranges[0].activeOffset); + + ranges[0].anchor->Release(); + ranges[0].active->Release(); CoTaskMemFree(ranges); ranges = nullptr; } @@ -2846,7 +2875,7 @@ IN_PROC_BROWSER_TEST_F(AccessibilityWinBrowserTest, } IN_PROC_BROWSER_TEST_F(AccessibilityWinBrowserTest, - DISABLED_TestStaticTextSetSelectionRanges) { + TestStaticTextSetSelectionRanges) { Microsoft::WRL::ComPtr<IAccessibleText> paragraph_text; SetUpSampleParagraph(¶graph_text); Microsoft::WRL::ComPtr<IAccessible2_4> ax_paragraph; @@ -2856,6 +2885,20 @@ IN_PROC_BROWSER_TEST_F(AccessibilityWinBrowserTest, ASSERT_HRESULT_SUCCEEDED(ax_paragraph->get_accChildCount(&child_count)); ASSERT_LT(0, child_count); + // IAccessible retrieves children using an one-based index. + base::win::ScopedVariant one_variant(1); + base::win::ScopedVariant child_count_variant(child_count); + + Microsoft::WRL::ComPtr<IDispatch> ax_first_static_text_child; + ASSERT_HRESULT_SUCCEEDED( + ax_paragraph->get_accChild(one_variant, &ax_first_static_text_child)); + ASSERT_NE(nullptr, ax_first_static_text_child); + + Microsoft::WRL::ComPtr<IDispatch> ax_last_static_text_child; + ASSERT_HRESULT_SUCCEEDED(ax_paragraph->get_accChild( + child_count_variant, &ax_last_static_text_child)); + ASSERT_NE(nullptr, ax_last_static_text_child); + LONG n_ranges = 1; IA2Range* ranges = reinterpret_cast<IA2Range*>(CoTaskMemAlloc(sizeof(IA2Range))); @@ -2868,6 +2911,8 @@ IN_PROC_BROWSER_TEST_F(AccessibilityWinBrowserTest, ranges[0].activeOffset = child_count + 1; EXPECT_HRESULT_FAILED(ax_paragraph->setSelectionRanges(n_ranges, ranges)); + // Select the entire paragraph's contents but not the paragraph itself, i.e. + // in the selected HTML the "p" tag will not be included. ranges[0].activeOffset = child_count; AccessibilityNotificationWaiter waiter( shell()->web_contents(), ui::kAXModeComplete, @@ -2882,14 +2927,21 @@ IN_PROC_BROWSER_TEST_F(AccessibilityWinBrowserTest, EXPECT_EQ(S_OK, hr); EXPECT_EQ(1, n_ranges); ASSERT_NE(nullptr, ranges); - EXPECT_EQ(ax_paragraph.Get(), ranges[0].anchor); + ASSERT_NE(nullptr, ranges[0].anchor); + EXPECT_EQ(ax_first_static_text_child.Get(), ranges[0].anchor); EXPECT_EQ(0, ranges[0].anchorOffset); + ASSERT_NE(nullptr, ranges[0].active); EXPECT_EQ(ax_paragraph.Get(), ranges[0].active); EXPECT_EQ(child_count, ranges[0].activeOffset); + ranges[0].anchor->Release(); + ranges[0].active->Release(); n_ranges = 1; ranges = reinterpret_cast<IA2Range*>(CoTaskMemRealloc(ranges, sizeof(IA2Range))); + + // Select from the beginning of the paragraph's text up to the start of the + // last static text child. ranges[0].anchor = ax_paragraph.Get(); ranges[0].anchorOffset = child_count - 1; ranges[0].active = ax_paragraph.Get(); @@ -2904,11 +2956,344 @@ IN_PROC_BROWSER_TEST_F(AccessibilityWinBrowserTest, EXPECT_EQ(S_OK, hr); EXPECT_EQ(1, n_ranges); ASSERT_NE(nullptr, ranges); - EXPECT_EQ(ax_paragraph.Get(), ranges[0].anchor); - EXPECT_EQ(static_cast<LONG>(InputContentsString().size() - 1), - ranges[0].anchorOffset); - EXPECT_EQ(ax_paragraph.Get(), ranges[0].active); + ASSERT_NE(nullptr, ranges[0].anchor); + EXPECT_EQ(ax_last_static_text_child.Get(), ranges[0].anchor); + EXPECT_EQ(0, ranges[0].anchorOffset); + ASSERT_NE(nullptr, ranges[0].active); + EXPECT_EQ(ax_first_static_text_child.Get(), ranges[0].active); + EXPECT_EQ(0, ranges[0].activeOffset); + + ranges[0].anchor->Release(); + ranges[0].active->Release(); + CoTaskMemFree(ranges); + ranges = nullptr; +} + +IN_PROC_BROWSER_TEST_F(AccessibilityWinBrowserTest, + SetSelectionWithIgnoredObjects) { + LoadInitialAccessibilityTreeFromHtml(R"HTML(<!DOCTYPE html> + <html> + <body> + <ul> + <li> + <div role="presentation"></div> + <p role="presentation"> + <span>Banana</span> + </p> + <span>fruit.</span> + </li> + </ul> + </body> + </html>)HTML"); + + BrowserAccessibility* list_item = FindNode(ax::mojom::Role::kListItem, ""); + ASSERT_NE(nullptr, list_item); + gfx::NativeViewAccessible list_item_win = + list_item->GetNativeViewAccessible(); + ASSERT_NE(nullptr, list_item_win); + + Microsoft::WRL::ComPtr<IAccessibleText> list_item_text; + ASSERT_HRESULT_SUCCEEDED( + list_item_win->QueryInterface(IID_PPV_ARGS(&list_item_text))); + + // The hypertext expose by "list_item_text" includes an embedded object + // character for the list bullet and the joined word "Bananafruit.". The word + // "Banana" is exposed as text because its container paragraph is ignored. + LONG n_characters; + ASSERT_HRESULT_SUCCEEDED(list_item_text->get_nCharacters(&n_characters)); + ASSERT_EQ(13, n_characters); + + AccessibilityNotificationWaiter waiter( + shell()->web_contents(), ui::kAXModeComplete, + ax::mojom::Event::kDocumentSelectionChanged); + + // First select the whole of the text found in the hypertext. + LONG start_offset = 0; + LONG end_offset = n_characters; + EXPECT_HRESULT_SUCCEEDED( + list_item_text->setSelection(0, start_offset, end_offset)); + waiter.WaitForNotification(); + + HRESULT hr = list_item_text->get_selection(0, &start_offset, &end_offset); + EXPECT_EQ(S_OK, hr); + EXPECT_EQ(0, start_offset); + EXPECT_EQ(n_characters, end_offset); + + // Select only the list bullet. + start_offset = 0; + end_offset = 1; + EXPECT_HRESULT_SUCCEEDED( + list_item_text->setSelection(0, start_offset, end_offset)); + waiter.WaitForNotification(); + + hr = list_item_text->get_selection(0, &start_offset, &end_offset); + EXPECT_EQ(S_OK, hr); + EXPECT_EQ(0, start_offset); + EXPECT_EQ(1, end_offset); + + // Select the word "Banana" in the ignored paragraph. + start_offset = 1; + end_offset = 7; + EXPECT_HRESULT_SUCCEEDED( + list_item_text->setSelection(0, start_offset, end_offset)); + waiter.WaitForNotification(); + + hr = list_item_text->get_selection(0, &start_offset, &end_offset); + EXPECT_EQ(S_OK, hr); + EXPECT_EQ(1, start_offset); + EXPECT_EQ(7, end_offset); + + // Select both the list bullet and the word "Banana" in the ignored paragraph. + start_offset = 0; + end_offset = 7; + EXPECT_HRESULT_SUCCEEDED( + list_item_text->setSelection(0, start_offset, end_offset)); + waiter.WaitForNotification(); + + hr = list_item_text->get_selection(0, &start_offset, &end_offset); + EXPECT_EQ(S_OK, hr); + EXPECT_EQ(0, start_offset); + EXPECT_EQ(7, end_offset); + + // Select the joined word "Bananafruit." both in the ignored paragraph and in + // the unignored span. + start_offset = 1; + end_offset = n_characters; + EXPECT_HRESULT_SUCCEEDED( + list_item_text->setSelection(0, start_offset, end_offset)); + waiter.WaitForNotification(); + + hr = list_item_text->get_selection(0, &start_offset, &end_offset); + EXPECT_EQ(S_OK, hr); + EXPECT_EQ(1, start_offset); + EXPECT_EQ(n_characters, end_offset); +} + +IN_PROC_BROWSER_TEST_F(AccessibilityWinBrowserTest, + SetSelectionRangesWithIgnoredObjects) { + LoadInitialAccessibilityTreeFromHtml(R"HTML(<!DOCTYPE html> + <html> + <body> + <ul> + <li> + <div role="presentation"></div> + <p role="presentation"> + <span>Banana</span> + </p> + <span>fruit.</span> + </li> + </ul> + </body> + </html>)HTML"); + + BrowserAccessibility* list_item = FindNode(ax::mojom::Role::kListItem, ""); + ASSERT_NE(nullptr, list_item); + BrowserAccessibility* list = list_item->PlatformGetParent(); + ASSERT_NE(nullptr, list); + + gfx::NativeViewAccessible list_item_win = + list_item->GetNativeViewAccessible(); + ASSERT_NE(nullptr, list_item_win); + gfx::NativeViewAccessible list_win = list->GetNativeViewAccessible(); + ASSERT_NE(nullptr, list_win); + + Microsoft::WRL::ComPtr<IAccessible2_4> ax_list_item; + ASSERT_HRESULT_SUCCEEDED( + list_item_win->QueryInterface(IID_PPV_ARGS(&ax_list_item))); + + Microsoft::WRL::ComPtr<IAccessible2_4> ax_list; + ASSERT_HRESULT_SUCCEEDED(list_win->QueryInterface(IID_PPV_ARGS(&ax_list))); + + // The list item should contain the list bullet and two static text objects + // containing the word "Banana" and the word "fruit". The first static text's + // immediate parent, i.e. the paragraph object, is ignored. + LONG child_count = 0; + ASSERT_HRESULT_SUCCEEDED(ax_list_item->get_accChildCount(&child_count)); + ASSERT_EQ(3, child_count); + + AccessibilityNotificationWaiter waiter( + shell()->web_contents(), ui::kAXModeComplete, + ax::mojom::Event::kDocumentSelectionChanged); + LONG n_ranges = 1; + IA2Range* ranges = + reinterpret_cast<IA2Range*>(CoTaskMemAlloc(sizeof(IA2Range))); + + // First select the whole of the list item. + ranges[0].anchor = ax_list_item.Get(); + ranges[0].anchorOffset = 0; + ranges[0].active = ax_list_item.Get(); + ranges[0].activeOffset = child_count; + EXPECT_HRESULT_SUCCEEDED(ax_list_item->setSelectionRanges(n_ranges, ranges)); + waiter.WaitForNotification(); + + CoTaskMemFree(ranges); + ranges = nullptr; + n_ranges = 0; + + HRESULT hr = ax_list->get_selectionRanges(&ranges, &n_ranges); + EXPECT_EQ(S_OK, hr); + EXPECT_EQ(1, n_ranges); + ASSERT_NE(nullptr, ranges); + ASSERT_NE(nullptr, ranges[0].anchor); + // The list bullet is not included in the DOM tree, so a DOM equivalent + // position at the beginning of the list (before the <li>) is computed by + // Blink. + EXPECT_EQ(ax_list.Get(), ranges[0].anchor); + EXPECT_EQ(0, ranges[0].anchorOffset); + ASSERT_NE(nullptr, ranges[0].active); + EXPECT_EQ(ax_list_item.Get(), ranges[0].active); + EXPECT_EQ(child_count, ranges[0].activeOffset); + + ranges[0].anchor->Release(); + ranges[0].active->Release(); + n_ranges = 1; + ranges = + reinterpret_cast<IA2Range*>(CoTaskMemRealloc(ranges, sizeof(IA2Range))); + + // Select only the list bullet. + ranges[0].anchor = ax_list_item.Get(); + ranges[0].anchorOffset = 0; + ranges[0].active = ax_list_item.Get(); + ranges[0].activeOffset = 1; + EXPECT_HRESULT_SUCCEEDED(ax_list_item->setSelectionRanges(n_ranges, ranges)); + waiter.WaitForNotification(); + + CoTaskMemFree(ranges); + ranges = nullptr; + n_ranges = 0; + + hr = ax_list->get_selectionRanges(&ranges, &n_ranges); + EXPECT_EQ(S_OK, hr); + EXPECT_EQ(1, n_ranges); + ASSERT_NE(nullptr, ranges); + ASSERT_NE(nullptr, ranges[0].anchor); + // The list bullet is not included in the DOM tree, so a DOM equivalent + // position at the beginning of the list (before the <li>) is computed by + // Blink. + EXPECT_EQ(ax_list.Get(), ranges[0].anchor); + EXPECT_EQ(0, ranges[0].anchorOffset); + ASSERT_NE(nullptr, ranges[0].active); + // Child 1 is the static text node with the word "Banana", so this is a + // "before text" position on that node. + // + // This is returned instead of an equivalent position anchored on the list + // item, in order to ensure that both a tree position before the first child + // and a "before text"position on the first child would always compare as + // equal. + EXPECT_EQ(list_item->PlatformGetChild(1)->GetNativeViewAccessible(), + ranges[0].active); + EXPECT_EQ(0, ranges[0].activeOffset); + + ranges[0].anchor->Release(); + ranges[0].active->Release(); + n_ranges = 1; + ranges = + reinterpret_cast<IA2Range*>(CoTaskMemRealloc(ranges, sizeof(IA2Range))); + + // Select the word "Banana" in the ignored paragraph. + ranges[0].anchor = ax_list_item.Get(); + ranges[0].anchorOffset = 1; + ranges[0].active = ax_list_item.Get(); + ranges[0].activeOffset = 2; + EXPECT_HRESULT_SUCCEEDED(ax_list_item->setSelectionRanges(n_ranges, ranges)); + waiter.WaitForNotification(); + + CoTaskMemFree(ranges); + ranges = nullptr; + n_ranges = 0; + + hr = ax_list->get_selectionRanges(&ranges, &n_ranges); + EXPECT_EQ(S_OK, hr); + EXPECT_EQ(1, n_ranges); + ASSERT_NE(nullptr, ranges); + ASSERT_NE(nullptr, ranges[0].anchor); + // Child 1 is the static text node with the word "Banana", so this is a + // "before text" position on that node. + EXPECT_EQ(list_item->PlatformGetChild(1)->GetNativeViewAccessible(), + ranges[0].anchor); + EXPECT_EQ(0, ranges[0].anchorOffset); + ASSERT_NE(nullptr, ranges[0].active); + // Child 2 is the static text node with the word "fruit.", so this is a + // "before text" position on that node. + // + // This is returned instead of an equivalent position anchored on the list + // item, in order to ensure that both a tree position before the second child + // and a "before text"position on the second child would always compare as + // equal. + EXPECT_EQ(list_item->PlatformGetChild(2)->GetNativeViewAccessible(), + ranges[0].active); + EXPECT_EQ(0, ranges[0].activeOffset); + + ranges[0].anchor->Release(); + ranges[0].active->Release(); + n_ranges = 1; + ranges = + reinterpret_cast<IA2Range*>(CoTaskMemRealloc(ranges, sizeof(IA2Range))); + + // Select the joined word "Bananafruit." both in the ignored paragraph and in + // the unignored span. + ranges[0].anchor = ax_list_item.Get(); + ranges[0].anchorOffset = 1; + ranges[0].active = ax_list_item.Get(); + ranges[0].activeOffset = child_count; + EXPECT_HRESULT_SUCCEEDED(ax_list_item->setSelectionRanges(n_ranges, ranges)); + waiter.WaitForNotification(); + + CoTaskMemFree(ranges); + ranges = nullptr; + n_ranges = 0; + + hr = ax_list->get_selectionRanges(&ranges, &n_ranges); + EXPECT_EQ(S_OK, hr); + EXPECT_EQ(1, n_ranges); + ASSERT_NE(nullptr, ranges); + ASSERT_NE(nullptr, ranges[0].anchor); + // Child 1 is the static text node with the word "Banana", so this is a + // "before text" position on that node. + EXPECT_EQ(list_item->PlatformGetChild(1)->GetNativeViewAccessible(), + ranges[0].anchor); + EXPECT_EQ(0, ranges[0].anchorOffset); + ASSERT_NE(nullptr, ranges[0].active); + EXPECT_EQ(ax_list_item.Get(), ranges[0].active); + EXPECT_EQ(child_count, ranges[0].activeOffset); + + ranges[0].anchor->Release(); + ranges[0].active->Release(); + n_ranges = 1; + ranges = + reinterpret_cast<IA2Range*>(CoTaskMemRealloc(ranges, sizeof(IA2Range))); + + // Select both the list bullet and the word "Banana" in the ignored paragraph. + ranges[0].anchor = ax_list_item.Get(); + ranges[0].anchorOffset = 0; + ranges[0].active = ax_list_item.Get(); + ranges[0].activeOffset = 2; + EXPECT_HRESULT_SUCCEEDED(ax_list_item->setSelectionRanges(n_ranges, ranges)); + waiter.WaitForNotification(); + + CoTaskMemFree(ranges); + ranges = nullptr; + n_ranges = 0; + + hr = ax_list->get_selectionRanges(&ranges, &n_ranges); + EXPECT_EQ(S_OK, hr); + EXPECT_EQ(1, n_ranges); + ASSERT_NE(nullptr, ranges); + ASSERT_NE(nullptr, ranges[0].anchor); + // The list bullet is not included in the DOM tree, so a DOM equivalent + // position at the beginning of the list (before the <li>) is computed by + // Blink. + EXPECT_EQ(ax_list.Get(), ranges[0].anchor); + EXPECT_EQ(0, ranges[0].anchorOffset); + ASSERT_NE(nullptr, ranges[0].active); + // Child 2 is the static text node with the word "fruit.", so this is a + // "before text" position on that node. + EXPECT_EQ(list_item->PlatformGetChild(2)->GetNativeViewAccessible(), + ranges[0].active); EXPECT_EQ(0, ranges[0].activeOffset); + + ranges[0].anchor->Release(); + ranges[0].active->Release(); CoTaskMemFree(ranges); ranges = nullptr; } @@ -3261,7 +3646,7 @@ IN_PROC_BROWSER_TEST_F(AccessibilityWinBrowserTest, // "Before". // // The embedded object character representing the image is at offset 6. - for (LONG i = 0; i <= 6; ++i) { + for (LONG i = 0; i < 6; ++i) { CheckTextAtOffset(contenteditable_text, i, IA2_TEXT_BOUNDARY_CHAR, i, (i + 1), std::wstring(1, expected_hypertext[i])); } @@ -3779,8 +4164,7 @@ IN_PROC_BROWSER_TEST_F(AccessibilityWinBrowserTest, HasHWNDAfterNavigation) { // At this point the root of the accessibility tree shouldn't have an HWND // because we never gave a parent window to the RWHVA. BrowserAccessibilityManagerWin* manager = - static_cast<BrowserAccessibilityManagerWin*>( - web_contents->GetRootBrowserAccessibilityManager()); + static_cast<BrowserAccessibilityManagerWin*>(GetManager()); ASSERT_EQ(nullptr, manager->GetParentHWND()); // Now add the RWHVA's window to the root window and ensure that we have @@ -4334,6 +4718,196 @@ IN_PROC_BROWSER_TEST_F(AccessibilityWinUIABrowserTest, TestGetFragmentRoot) { ASSERT_NE(nullptr, fragment_root.Get()); } +IN_PROC_BROWSER_TEST_F(AccessibilityWinUIABrowserTest, IA2ElementToUIAElement) { + // This test validates looking up an UIA element from an IA2 element. + // We start by retrieving an IA2 element then its corresponding unique id. We + // then use the unique id to retrieve the corresponding UIA element through + // IItemContainerProvider::FindItemByProperty(). + LoadInitialAccessibilityTreeFromHtml( + R"HTML( + <html> + <div role="button">button</div> + <div role="listbox" aria-label="listbox"></div> + </html> + )HTML"); + + // Obtain the fragment root from the top-level HWND. + HWND hwnd = shell()->window()->GetHost()->GetAcceleratedWidget(); + ASSERT_NE(gfx::kNullAcceleratedWidget, hwnd); + ui::AXFragmentRootWin* fragment_root = + ui::AXFragmentRootWin::GetForAcceleratedWidget(hwnd); + ASSERT_NE(nullptr, fragment_root); + + Microsoft::WRL::ComPtr<IItemContainerProvider> item_container_provider; + ASSERT_HRESULT_SUCCEEDED( + fragment_root->GetNativeViewAccessible()->QueryInterface( + IID_PPV_ARGS(&item_container_provider))); + + Microsoft::WRL::ComPtr<IAccessible> document(GetRendererAccessible()); + std::vector<base::win::ScopedVariant> document_children = + GetAllAccessibleChildren(document.Get()); + + // Look up button's UIA element from its IA2 element. + { + // Retrieve button's IA2 element. + Microsoft::WRL::ComPtr<IAccessible2> button_ia2; + ASSERT_HRESULT_SUCCEEDED(QueryIAccessible2( + GetAccessibleFromVariant(document.Get(), document_children[0].AsInput()) + .Get(), + &button_ia2)); + LONG button_role = 0; + ASSERT_HRESULT_SUCCEEDED(button_ia2->role(&button_role)); + ASSERT_EQ(ROLE_SYSTEM_PUSHBUTTON, button_role); + + // Retrieve button's IA2 unique id. + LONG button_unique_id; + button_ia2->get_uniqueID(&button_unique_id); + base::win::ScopedVariant ia2_unique_id; + ia2_unique_id.Set( + SysAllocString(base::NumberToString16(button_unique_id).c_str())); + + // Verify we can find the button's UIA element based on its unique id. + Microsoft::WRL::ComPtr<IRawElementProviderSimple> button_uia; + ASSERT_HRESULT_SUCCEEDED(item_container_provider->FindItemByProperty( + nullptr, ui::UiaRegistrarWin::GetInstance().GetUiaUniqueIdPropertyId(), + ia2_unique_id, &button_uia)); + + // UIA and IA2 elements should have the same unique id. + base::win::ScopedVariant uia_unique_id; + ASSERT_HRESULT_SUCCEEDED(button_uia->GetPropertyValue( + ui::UiaRegistrarWin::GetInstance().GetUiaUniqueIdPropertyId(), + uia_unique_id.Receive())); + ASSERT_STREQ(ia2_unique_id.ptr()->bstrVal, uia_unique_id.ptr()->bstrVal); + + // Verify the retrieved UIA element is button through its name property. + base::win::ScopedVariant name_property; + ASSERT_HRESULT_SUCCEEDED(button_uia->GetPropertyValue( + UIA_NamePropertyId, name_property.Receive())); + ASSERT_EQ(name_property.type(), VT_BSTR); + BSTR name_bstr = name_property.ptr()->bstrVal; + base::string16 actual_name(name_bstr, ::SysStringLen(name_bstr)); + ASSERT_EQ(L"button", actual_name); + + // Verify that the button's IA2 element and UIA element are the same through + // comparing their IUnknown interfaces. + Microsoft::WRL::ComPtr<IUnknown> iunknown_button_from_uia; + ASSERT_HRESULT_SUCCEEDED( + button_uia->QueryInterface(IID_PPV_ARGS(&iunknown_button_from_uia))); + + Microsoft::WRL::ComPtr<IUnknown> iunknown_button_from_ia2; + ASSERT_HRESULT_SUCCEEDED( + button_ia2->QueryInterface(IID_PPV_ARGS(&iunknown_button_from_ia2))); + + ASSERT_EQ(iunknown_button_from_uia.Get(), iunknown_button_from_ia2.Get()); + } + + // Look up listbox's UIA element from its IA2 element. + { + // Retrieve listbox's IA2 element. + Microsoft::WRL::ComPtr<IAccessible2> listbox_ia2; + ASSERT_HRESULT_SUCCEEDED(QueryIAccessible2( + GetAccessibleFromVariant(document.Get(), document_children[1].AsInput()) + .Get(), + &listbox_ia2)); + LONG listbox_role = 0; + ASSERT_HRESULT_SUCCEEDED(listbox_ia2->role(&listbox_role)); + ASSERT_EQ(ROLE_SYSTEM_LIST, listbox_role); + + // Retrieve listbox's IA2 unique id. + LONG listbox_unique_id; + listbox_ia2->get_uniqueID(&listbox_unique_id); + base::win::ScopedVariant ia2_unique_id; + ia2_unique_id.Set( + SysAllocString(base::NumberToString16(listbox_unique_id).c_str())); + + // Verify we can find the listbox's UIA element based on its unique id. + Microsoft::WRL::ComPtr<IRawElementProviderSimple> listbox_uia; + ASSERT_HRESULT_SUCCEEDED(item_container_provider->FindItemByProperty( + nullptr, ui::UiaRegistrarWin::GetInstance().GetUiaUniqueIdPropertyId(), + ia2_unique_id, &listbox_uia)); + + // UIA and IA2 elements should have the same unique id. + base::win::ScopedVariant uia_unique_id; + ASSERT_HRESULT_SUCCEEDED(listbox_uia->GetPropertyValue( + ui::UiaRegistrarWin::GetInstance().GetUiaUniqueIdPropertyId(), + uia_unique_id.Receive())); + ASSERT_STREQ(ia2_unique_id.ptr()->bstrVal, uia_unique_id.ptr()->bstrVal); + + // Verify the retrieved UIA element is listbox through its name property. + base::win::ScopedVariant name_property; + ASSERT_HRESULT_SUCCEEDED(listbox_uia->GetPropertyValue( + UIA_NamePropertyId, name_property.Receive())); + ASSERT_EQ(name_property.type(), VT_BSTR); + BSTR name_bstr = name_property.ptr()->bstrVal; + base::string16 actual_name(name_bstr, ::SysStringLen(name_bstr)); + ASSERT_EQ(L"listbox", actual_name); + + // Verify that the listbox's IA2 element and UIA element are the same + // through comparing their IUnknown interfaces. + Microsoft::WRL::ComPtr<IUnknown> iunknown_listbox_from_uia; + ASSERT_HRESULT_SUCCEEDED( + listbox_uia->QueryInterface(IID_PPV_ARGS(&iunknown_listbox_from_uia))); + + Microsoft::WRL::ComPtr<IUnknown> iunknown_listbox_from_ia2; + ASSERT_HRESULT_SUCCEEDED( + listbox_ia2->QueryInterface(IID_PPV_ARGS(&iunknown_listbox_from_ia2))); + + ASSERT_EQ(iunknown_listbox_from_uia.Get(), iunknown_listbox_from_ia2.Get()); + } +} + +IN_PROC_BROWSER_TEST_F(AccessibilityWinUIABrowserTest, UIAElementToIA2Element) { + // This test validates looking up an IA2 element from an UIA element. + // We start by retrieving an UIA element then its corresponding unique id. We + // then use the unique id to retrieve the corresponding IA2 element. + LoadInitialAccessibilityTreeFromHtml( + R"HTML( + <html> + <div role="button">button</div> + </html> + )HTML"); + Microsoft::WRL::ComPtr<IRawElementProviderSimple> button_uia = + QueryInterfaceFromNode<IRawElementProviderSimple>( + FindNode(ax::mojom::Role::kButton, "button")); + + // Retrieve the UIA element's unique id. + base::win::ScopedVariant uia_unique_id; + ASSERT_HRESULT_SUCCEEDED(button_uia->GetPropertyValue( + ui::UiaRegistrarWin::GetInstance().GetUiaUniqueIdPropertyId(), + uia_unique_id.Receive())); + + int32_t unique_id_value; + ASSERT_EQ(VT_BSTR, uia_unique_id.type()); + ASSERT_TRUE( + base::StringToInt(uia_unique_id.ptr()->bstrVal, &unique_id_value)); + + // Retrieve the corresponding IA2 element through the unique id. + Microsoft::WRL::ComPtr<IAccessible> document(GetRendererAccessible()); + base::win::ScopedVariant ia2_unique_id(unique_id_value); + Microsoft::WRL::ComPtr<IAccessible2> button_ia2; + ASSERT_HRESULT_SUCCEEDED(QueryIAccessible2( + GetAccessibleFromVariant(document.Get(), ia2_unique_id.AsInput()).Get(), + &button_ia2)); + + // Verify that the retrieved IA2 element shares the same unique id as the UIA + // element. + LONG button_ia2_unique_id; + button_ia2->get_uniqueID(&button_ia2_unique_id); + ASSERT_EQ(unique_id_value, button_ia2_unique_id); + + // Verify that the IA2 element and UIA element are the same through + // comparing their IUnknown interfaces. + Microsoft::WRL::ComPtr<IUnknown> iunknown_button_from_uia; + ASSERT_HRESULT_SUCCEEDED( + button_uia->QueryInterface(IID_PPV_ARGS(&iunknown_button_from_uia))); + + Microsoft::WRL::ComPtr<IUnknown> iunknown_button_from_ia2; + ASSERT_HRESULT_SUCCEEDED( + button_ia2->QueryInterface(IID_PPV_ARGS(&iunknown_button_from_ia2))); + + ASSERT_EQ(iunknown_button_from_uia.Get(), iunknown_button_from_ia2.Get()); +} + IN_PROC_BROWSER_TEST_F(AccessibilityWinUIABrowserTest, RootElementPropertyValues) { LoadInitialAccessibilityTreeFromHtml( @@ -4511,6 +5085,56 @@ IN_PROC_BROWSER_TEST_F(AccessibilityWinUIABrowserTest, ->GetAccessibilityMode()); } +IN_PROC_BROWSER_TEST_F(AccessibilityWinUIABrowserTest, + TabHeuristicForWindowsNarrator) { + // Windows Narrator uses certain heuristics to determine where in the UIA + // tree a "browser tab" begins and ends, in order to contain the search range + // for commands such as "move to next/previous text input field." + // This test is used to validate such heuristics. + // Specifically: The boundary of a browser tab is the element nearest the + // root with a ControlType of Document and an implementation of TextPattern. + // In this test, we validate that such an element exists. + + // Load an empty HTML page. + LoadInitialAccessibilityTreeFromHtml( + R"HTML(<!DOCTYPE html> + <html> + </html>)HTML"); + + // The element exposed by the legacy window is a fragment root whose first + // child represents the document root (our tab boundary). First, get the + // fragment root using the legacy window's HWND. + RenderWidgetHostViewAura* render_widget_host_view_aura = + static_cast<RenderWidgetHostViewAura*>( + shell()->web_contents()->GetRenderWidgetHostView()); + HWND hwnd = render_widget_host_view_aura->AccessibilityGetAcceleratedWidget(); + ASSERT_NE(gfx::kNullAcceleratedWidget, hwnd); + Microsoft::WRL::ComPtr<IUIAutomation> uia; + ASSERT_HRESULT_SUCCEEDED(CoCreateInstance(CLSID_CUIAutomation, nullptr, + CLSCTX_INPROC_SERVER, + IID_IUIAutomation, &uia)); + Microsoft::WRL::ComPtr<IUIAutomationElement> fragment_root; + ASSERT_HRESULT_SUCCEEDED(uia->ElementFromHandle(hwnd, &fragment_root)); + ASSERT_NE(nullptr, fragment_root.Get()); + + // Now get the fragment root's first (only) child. + Microsoft::WRL::ComPtr<IUIAutomationTreeWalker> tree_walker; + uia->get_RawViewWalker(&tree_walker); + Microsoft::WRL::ComPtr<IUIAutomationElement> first_child; + tree_walker->GetFirstChildElement(fragment_root.Get(), &first_child); + ASSERT_NE(nullptr, first_child.Get()); + + // Validate the control type and presence of TextPattern. + CONTROLTYPEID control_type; + ASSERT_HRESULT_SUCCEEDED(first_child->get_CurrentControlType(&control_type)); + EXPECT_EQ(control_type, UIA_DocumentControlTypeId); + + Microsoft::WRL::ComPtr<IUnknown> text_pattern_unknown; + ASSERT_HRESULT_SUCCEEDED( + first_child->GetCurrentPattern(UIA_TextPatternId, &text_pattern_unknown)); + EXPECT_NE(nullptr, text_pattern_unknown.Get()); +} + IN_PROC_BROWSER_TEST_F(AccessibilityWinBrowserTest, TestOffsetsOfSelectionAll) { LoadInitialAccessibilityTreeFromHtml(R"HTML( <p>Hello world.</p> diff --git a/chromium/content/browser/accessibility/ax_platform_node_textrangeprovider_win_browsertest.cc b/chromium/content/browser/accessibility/ax_platform_node_textrangeprovider_win_browsertest.cc index b1772aa9847..164a92b2a15 100644 --- a/chromium/content/browser/accessibility/ax_platform_node_textrangeprovider_win_browsertest.cc +++ b/chromium/content/browser/accessibility/ax_platform_node_textrangeprovider_win_browsertest.cc @@ -526,10 +526,16 @@ IN_PROC_BROWSER_TEST_F(AXPlatformNodeTextRangeProviderWinBrowserTest, <input type='text' aria-label='input_text'><span style="font-size: 12pt">Text1</span> </div> + <div contenteditable="true"> + <ul><li>item</li></ul>3.14 + </div> </body> </html> )HTML")); + // Case 1: Inside of a plain text field, NormalizeTextRange shouldn't modify + // the text range endpoints. + // // In order for the test harness to effectively simulate typing in a text // input, first change the value of the text input and then focus it. Only // editing the value won't show the cursor and only focusing will put the @@ -570,7 +576,7 @@ IN_PROC_BROWSER_TEST_F(AXPlatformNodeTextRangeProviderWinBrowserTest, /*expected_text*/ L"", /*expected_count*/ 4); - // Clone the original text range so we can keep track if NormalizeEndpoints + // Clone the original text range so we can keep track if NormalizeTextRange // causes a change in position. ComPtr<ITextRangeProvider> text_range_provider_clone; text_range_provider->Clone(&text_range_provider_clone); @@ -584,7 +590,8 @@ IN_PROC_BROWSER_TEST_F(AXPlatformNodeTextRangeProviderWinBrowserTest, ASSERT_EQ(0, result); // Calling GetAttributeValue will call NormalizeTextRange, which shouldn't - // change the result of CompareEndpoints below. + // change the result of CompareEndpoints below since the range is inside a + // plain text field. base::win::ScopedVariant value; EXPECT_HRESULT_SUCCEEDED(text_range_provider->GetAttributeValue( UIA_IsReadOnlyAttributeId, value.Receive())); @@ -592,16 +599,56 @@ IN_PROC_BROWSER_TEST_F(AXPlatformNodeTextRangeProviderWinBrowserTest, EXPECT_EQ(V_BOOL(value.ptr()), VARIANT_FALSE); value.Reset(); - EXPECT_HRESULT_SUCCEEDED(text_range_provider_clone->GetAttributeValue( + EXPECT_HRESULT_SUCCEEDED(text_range_provider->CompareEndpoints( + TextPatternRangeEndpoint_End, text_range_provider_clone.Get(), + TextPatternRangeEndpoint_Start, &result)); + ASSERT_EQ(0, result); + + // Case 2: Inside of a rich text field, NormalizeTextRange should modify the + // text range endpoints. + auto* node = FindNode(ax::mojom::Role::kStaticText, "item"); + ASSERT_NE(nullptr, node); + EXPECT_TRUE(node->PlatformIsLeaf()); + EXPECT_EQ(0u, node->PlatformChildCount()); + + GetTextRangeProviderFromTextNode(*node, &text_range_provider); + ASSERT_NE(nullptr, text_range_provider.Get()); + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"item"); + + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_Start, TextUnit_Character, + /*count*/ 4, + /*expected_text*/ L"", + /*expected_count*/ 4); + // Make the range degenerate. + EXPECT_UIA_MOVE_ENDPOINT_BY_UNIT( + text_range_provider, TextPatternRangeEndpoint_End, TextUnit_Character, + /*count*/ 1, + /*expected_text*/ L"\n3", + /*expected_count*/ 1); + + // The range should now span two nodes: start: "item<>", end: "<3>.14". + EXPECT_UIA_TEXTRANGE_EQ(text_range_provider, L"\n3"); + + // Clone the original text range so we can keep track if NormalizeTextRange + // causes a change in position. + text_range_provider->Clone(&text_range_provider_clone); + + // Calling GetAttributeValue will call NormalizeTextRange, which should + // change the result of CompareEndpoints below since we are in a rich text + // field. + EXPECT_HRESULT_SUCCEEDED(text_range_provider->GetAttributeValue( UIA_IsReadOnlyAttributeId, value.Receive())); EXPECT_EQ(value.type(), VT_BOOL); EXPECT_EQ(V_BOOL(value.ptr()), VARIANT_FALSE); value.Reset(); + // Since text_range_provider has been modified by NormalizeTextRange, we + // expect a difference here. EXPECT_HRESULT_SUCCEEDED(text_range_provider->CompareEndpoints( TextPatternRangeEndpoint_End, text_range_provider_clone.Get(), TextPatternRangeEndpoint_Start, &result)); - ASSERT_EQ(0, result); + ASSERT_EQ(1, result); } IN_PROC_BROWSER_TEST_F(AXPlatformNodeTextRangeProviderWinBrowserTest, diff --git a/chromium/content/browser/accessibility/ax_platform_node_win_browsertest.cc b/chromium/content/browser/accessibility/ax_platform_node_win_browsertest.cc index e8774c8dc33..c83b531e584 100644 --- a/chromium/content/browser/accessibility/ax_platform_node_win_browsertest.cc +++ b/chromium/content/browser/accessibility/ax_platform_node_win_browsertest.cc @@ -21,6 +21,17 @@ using Microsoft::WRL::ComPtr; namespace content { + +#define EXPECT_UIA_INT_EQ(node, property_id, expected) \ + { \ + base::win::ScopedVariant expectedVariant(expected); \ + ASSERT_EQ(VT_I4, expectedVariant.type()); \ + base::win::ScopedVariant actual; \ + ASSERT_HRESULT_SUCCEEDED( \ + node->GetPropertyValue(property_id, actual.Receive())); \ + EXPECT_EQ(expectedVariant.ptr()->intVal, actual.ptr()->intVal); \ + } + class AXPlatformNodeWinBrowserTest : public AccessibilityContentBrowserTest { protected: template <typename T> @@ -48,6 +59,19 @@ class AXPlatformNodeWinBrowserTest : public AccessibilityContentBrowserTest { return result; } + BrowserAccessibility* FindNodeAfter(BrowserAccessibility* begin, + const std::string& name) { + WebContentsImpl* web_contents = + static_cast<WebContentsImpl*>(shell()->web_contents()); + BrowserAccessibilityManager* manager = + web_contents->GetRootBrowserAccessibilityManager(); + BrowserAccessibility* node = begin; + while (node && (node->GetName() != name)) + node = manager->NextInTreeOrder(node); + + return node; + } + void UIAGetPropertyValueFlowsFromBrowserTestTemplate( const BrowserAccessibility* target_browser_accessibility, const std::vector<std::string>& expected_names) { @@ -431,6 +455,77 @@ IN_PROC_BROWSER_TEST_F(AXPlatformNodeWinBrowserTest, } IN_PROC_BROWSER_TEST_F(AXPlatformNodeWinBrowserTest, + UIAGetPropertyValueCulture) { + LoadInitialAccessibilityTreeFromHtml(std::string(R"HTML( + <!DOCTYPE html> + <html> + <body> + <div lang='en-us'>en-us</div> + <div lang='en-gb'>en-gb</div> + <div lang='ru-ru'>ru-ru</div> + <div lang='fake'>fake</div> + <div>no lang</div> + <div lang=''>empty lang</div> + </body> + </html> + )HTML")); + + BrowserAccessibility* root_node = GetRootAndAssertNonNull(); + BrowserAccessibility* body_node = root_node->PlatformGetFirstChild(); + ASSERT_NE(nullptr, body_node); + + BrowserAccessibility* node = FindNodeAfter(body_node, "en-us"); + ASSERT_NE(nullptr, node); + BrowserAccessibilityComWin* en_us_node_com_win = + ToBrowserAccessibilityWin(node)->GetCOM(); + ASSERT_NE(nullptr, en_us_node_com_win); + constexpr int en_us_lcid = 1033; + EXPECT_UIA_INT_EQ(en_us_node_com_win, UIA_CulturePropertyId, en_us_lcid); + + node = FindNodeAfter(node, "en-gb"); + ASSERT_NE(nullptr, node); + BrowserAccessibilityComWin* en_gb_node_com_win = + ToBrowserAccessibilityWin(node)->GetCOM(); + ASSERT_NE(nullptr, en_gb_node_com_win); + constexpr int en_gb_lcid = 2057; + EXPECT_UIA_INT_EQ(en_gb_node_com_win, UIA_CulturePropertyId, en_gb_lcid); + + node = FindNodeAfter(node, "ru-ru"); + ASSERT_NE(nullptr, node); + BrowserAccessibilityComWin* ru_ru_node_com_win = + ToBrowserAccessibilityWin(node)->GetCOM(); + ASSERT_NE(nullptr, ru_ru_node_com_win); + constexpr int ru_ru_lcid = 1049; + EXPECT_UIA_INT_EQ(ru_ru_node_com_win, UIA_CulturePropertyId, ru_ru_lcid); + + // Setting to an invalid language should return a failed HRESULT. + node = FindNodeAfter(node, "fake"); + ASSERT_NE(nullptr, node); + BrowserAccessibilityComWin* fake_lang_node_com_win = + ToBrowserAccessibilityWin(node)->GetCOM(); + ASSERT_NE(nullptr, fake_lang_node_com_win); + base::win::ScopedVariant actual; + EXPECT_HRESULT_FAILED(fake_lang_node_com_win->GetPropertyValue( + UIA_CulturePropertyId, actual.Receive())); + + // No lang should default to the page's default language (en-us). + node = FindNodeAfter(node, "no lang"); + ASSERT_NE(nullptr, node); + BrowserAccessibilityComWin* no_lang_node_com_win = + ToBrowserAccessibilityWin(node)->GetCOM(); + ASSERT_NE(nullptr, no_lang_node_com_win); + EXPECT_UIA_INT_EQ(no_lang_node_com_win, UIA_CulturePropertyId, en_us_lcid); + + // Empty lang should default to the page's default language (en-us). + node = FindNodeAfter(node, "empty lang"); + ASSERT_NE(nullptr, node); + BrowserAccessibilityComWin* empty_lang_node_com_win = + ToBrowserAccessibilityWin(node)->GetCOM(); + ASSERT_NE(nullptr, empty_lang_node_com_win); + EXPECT_UIA_INT_EQ(empty_lang_node_com_win, UIA_CulturePropertyId, en_us_lcid); +} + +IN_PROC_BROWSER_TEST_F(AXPlatformNodeWinBrowserTest, HitTestOnAncestorOfWebRoot) { EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL))); diff --git a/chromium/content/browser/accessibility/browser_accessibility.cc b/chromium/content/browser/accessibility/browser_accessibility.cc index 64000eb7dff..a84e5a94575 100644 --- a/chromium/content/browser/accessibility/browser_accessibility.cc +++ b/chromium/content/browser/accessibility/browser_accessibility.cc @@ -101,65 +101,24 @@ int GetBoundaryTextOffsetInsideBaseAnchor( void BrowserAccessibility::Init(BrowserAccessibilityManager* manager, ui::AXNode* node) { + DCHECK(manager); + DCHECK(node); manager_ = manager; node_ = node; } bool BrowserAccessibility::PlatformIsLeaf() const { - if (InternalChildCount() == 0) - return true; - - return PlatformIsLeafIncludingIgnored(); + // TODO(nektar): Remove in favor of IsLeaf. + return IsLeaf(); } bool BrowserAccessibility::PlatformIsLeafIncludingIgnored() const { - if (node()->children().size() == 0) - return true; - - // These types of objects may have children that we use as internal - // implementation details, but we want to expose them as leaves to platform - // accessibility APIs because screen readers might be confused if they find - // any children. - if (IsPlainTextField() || IsTextOnlyObject()) - return true; - - // Roles whose children are only presentational according to the ARIA and - // HTML5 Specs should be hidden from screen readers. - switch (GetRole()) { - // According to the ARIA and Core-AAM specs: - // https://w3c.github.io/aria/#button, - // https://www.w3.org/TR/core-aam-1.1/#exclude_elements - // button's children are presentational only and should be hidden from - // screen readers. However, we cannot enforce the leafiness of buttons - // because they may contain many rich, interactive descendants such as a day - // in a calendar, and screen readers will need to interact with these - // contents. See https://crbug.com/689204. - // So we decided to not enforce the leafiness of buttons and expose all - // children. The only exception to enforce leafiness is when the button has - // a single text child and to prevent screen readers from double speak. - case ax::mojom::Role::kButton: { - if (InternalChildCount() == 1 && - InternalGetFirstChild()->IsTextOnlyObject()) - return true; - return false; - } - case ax::mojom::Role::kDocCover: - case ax::mojom::Role::kGraphicsSymbol: - case ax::mojom::Role::kImage: - case ax::mojom::Role::kMeter: - case ax::mojom::Role::kScrollBar: - case ax::mojom::Role::kSlider: - case ax::mojom::Role::kSplitter: - case ax::mojom::Role::kProgressIndicator: - return true; - default: - return false; - } + return node()->IsLeafIncludingIgnored(); } bool BrowserAccessibility::CanFireEvents() const { // Allow events unless this object would be trimmed away. - return !PlatformIsChildOfLeafIncludingIgnored(); + return !IsChildOfLeaf(); } ui::AXPlatformNode* BrowserAccessibility::GetAXPlatformNode() const { @@ -179,11 +138,11 @@ uint32_t BrowserAccessibility::PlatformChildCount() const { } BrowserAccessibility* BrowserAccessibility::PlatformGetParent() const { - ui::AXNode* parent = node_->GetUnignoredParent(); + ui::AXNode* parent = node()->GetUnignoredParent(); if (parent) - return manager_->GetFromAXNode(parent); + return manager()->GetFromAXNode(parent); - return manager_->GetParentNodeFromParentTree(); + return manager()->GetParentNodeFromParentTree(); } BrowserAccessibility* BrowserAccessibility::PlatformGetFirstChild() const { @@ -249,11 +208,11 @@ bool BrowserAccessibility::IsIgnored() const { } bool BrowserAccessibility::IsTextOnlyObject() const { - return node_ && node_->IsText(); + return node()->IsText(); } bool BrowserAccessibility::IsLineBreakObject() const { - return node_ && node_->IsLineBreak(); + return node()->IsLineBreak(); } BrowserAccessibility* BrowserAccessibility::PlatformGetChild( @@ -274,18 +233,6 @@ BrowserAccessibility* BrowserAccessibility::PlatformGetChild( return result; } -bool BrowserAccessibility::PlatformIsChildOfLeafIncludingIgnored() const { - BrowserAccessibility* ancestor = InternalGetParent(); - - while (ancestor) { - if (ancestor->PlatformIsLeafIncludingIgnored()) - return true; - ancestor = ancestor->InternalGetParent(); - } - - return false; -} - BrowserAccessibility* BrowserAccessibility::PlatformGetClosestPlatformObject() const { BrowserAccessibility* platform_object = @@ -449,7 +396,7 @@ BrowserAccessibility::InternalChildrenEnd() const { } int32_t BrowserAccessibility::GetId() const { - return node_ ? node_->id() : -1; + return node()->id(); } gfx::RectF BrowserAccessibility::GetLocation() const { @@ -1077,11 +1024,11 @@ bool BrowserAccessibility::IsClickable() const { } bool BrowserAccessibility::IsTextField() const { - return IsPlainTextField() || IsRichTextField(); + return GetData().IsTextField(); } bool BrowserAccessibility::IsPasswordField() const { - return IsTextField() && HasState(ax::mojom::State::kProtected); + return GetData().IsPasswordField(); } bool BrowserAccessibility::IsPlainTextField() const { @@ -1089,8 +1036,7 @@ bool BrowserAccessibility::IsPlainTextField() const { } bool BrowserAccessibility::IsRichTextField() const { - return GetBoolAttribute(ax::mojom::BoolAttribute::kEditableRoot) && - HasState(ax::mojom::State::kRichlyEditable); + return GetData().IsRichTextField(); } bool BrowserAccessibility::HasExplicitlyEmptyName() const { @@ -1199,18 +1145,7 @@ base::string16 BrowserAccessibility::GetHypertext() const { } base::string16 BrowserAccessibility::GetInnerText() const { - if (!InternalChildCount()) { - if (IsTextField()) - return GetString16Attribute(ax::mojom::StringAttribute::kValue); - return GetString16Attribute(ax::mojom::StringAttribute::kName); - } - - base::string16 text; - for (InternalChildIterator it = InternalChildrenBegin(); - it != InternalChildrenEnd(); ++it) { - text += (*it).GetInnerText(); - } - return text; + return base::UTF8ToUTF16(node()->GetInnerText()); } gfx::Rect BrowserAccessibility::RelativeToAbsoluteBounds( @@ -1488,10 +1423,43 @@ const ui::AXTreeData& BrowserAccessibility::GetTreeData() const { const ui::AXTree::Selection BrowserAccessibility::GetUnignoredSelection() const { - if (manager()) - return manager()->ax_tree()->GetUnignoredSelection(); - return ui::AXTree::Selection{-1, -1, -1, - ax::mojom::TextAffinity::kDownstream}; + DCHECK(manager()); + ui::AXTree::Selection selection = + manager()->ax_tree()->GetUnignoredSelection(); + + // "selection.anchor_offset" and "selection.focus_ofset" might need to be + // adjusted if the anchor or the focus nodes include ignored children. + const BrowserAccessibility* anchor_object = + manager()->GetFromID(selection.anchor_object_id); + if (anchor_object && !anchor_object->PlatformIsLeaf()) { + DCHECK_GE(selection.anchor_offset, 0); + if (size_t{selection.anchor_offset} < + anchor_object->node()->children().size()) { + const ui::AXNode* anchor_child = + anchor_object->node()->children()[selection.anchor_offset]; + DCHECK(anchor_child); + selection.anchor_offset = int{anchor_child->GetUnignoredIndexInParent()}; + } else { + selection.anchor_offset = anchor_object->GetChildCount(); + } + } + + const BrowserAccessibility* focus_object = + manager()->GetFromID(selection.focus_object_id); + if (focus_object && !focus_object->PlatformIsLeaf()) { + DCHECK_GE(selection.focus_offset, 0); + if (size_t{selection.focus_offset} < + focus_object->node()->children().size()) { + const ui::AXNode* focus_child = + focus_object->node()->children()[selection.focus_offset]; + DCHECK(focus_child); + selection.focus_offset = int{focus_child->GetUnignoredIndexInParent()}; + } else { + selection.focus_offset = focus_object->GetChildCount(); + } + } + + return selection; } ui::AXNodePosition::AXPositionInstance @@ -1521,7 +1489,7 @@ gfx::NativeViewAccessible BrowserAccessibility::GetParent() { } int BrowserAccessibility::GetChildCount() const { - return PlatformChildCount(); + return int{PlatformChildCount()}; } gfx::NativeViewAccessible BrowserAccessibility::ChildAtIndex(int index) { @@ -1565,15 +1533,31 @@ gfx::NativeViewAccessible BrowserAccessibility::GetPreviousSibling() { } bool BrowserAccessibility::IsChildOfLeaf() const { - BrowserAccessibility* ancestor = InternalGetParent(); - - while (ancestor) { - if (ancestor->PlatformIsLeaf()) - return true; - ancestor = ancestor->InternalGetParent(); + return node()->IsChildOfLeaf(); +} + +bool BrowserAccessibility::IsLeaf() const { + // According to the ARIA and Core-AAM specs: + // https://w3c.github.io/aria/#button, + // https://www.w3.org/TR/core-aam-1.1/#exclude_elements + // button's children are presentational only and should be hidden from + // screen readers. However, we cannot enforce the leafiness of buttons + // because they may contain many rich, interactive descendants such as a day + // in a calendar, and screen readers will need to interact with these + // contents. See https://crbug.com/689204. + // So we decided to not enforce the leafiness of buttons and expose all + // children. The only exception to enforce leafiness is when the button has + // a single text child and to prevent screen readers from double speak. + if (GetRole() == ax::mojom::Role::kButton) { + return InternalChildCount() == 1 && + InternalGetFirstChild()->IsTextOnlyObject(); } + return node()->IsLeaf(); +} - return false; +bool BrowserAccessibility::IsChildOfPlainTextField() const { + ui::AXNode* textfield_node = node()->GetTextFieldAncestor(); + return textfield_node && textfield_node->data().IsPlainTextField(); } gfx::NativeViewAccessible BrowserAccessibility::GetClosestPlatformObject() @@ -1705,7 +1689,7 @@ int BrowserAccessibility::GetIndexInParent() { // index at AXPlatformNodeBase. return -1; } - return node_ ? node_->GetUnignoredIndexInParent() : -1; + return node()->GetUnignoredIndexInParent(); } gfx::AcceleratedWidget @@ -1746,30 +1730,24 @@ base::Optional<bool> BrowserAccessibility::GetTableHasColumnOrRowHeaderNode() return node()->GetTableHasColumnOrRowHeaderNode(); } -std::vector<int32_t> BrowserAccessibility::GetColHeaderNodeIds() const { - std::vector<int32_t> result; - node()->GetTableCellColHeaderNodeIds(&result); - return result; +std::vector<ui::AXNode::AXID> BrowserAccessibility::GetColHeaderNodeIds() + const { + return node()->GetTableColHeaderNodeIds(); } -std::vector<int32_t> BrowserAccessibility::GetColHeaderNodeIds( +std::vector<ui::AXNode::AXID> BrowserAccessibility::GetColHeaderNodeIds( int col_index) const { - std::vector<int32_t> result; - node()->GetTableColHeaderNodeIds(col_index, &result); - return result; + return node()->GetTableColHeaderNodeIds(col_index); } -std::vector<int32_t> BrowserAccessibility::GetRowHeaderNodeIds() const { - std::vector<int32_t> result; - node()->GetTableCellRowHeaderNodeIds(&result); - return result; +std::vector<ui::AXNode::AXID> BrowserAccessibility::GetRowHeaderNodeIds() + const { + return node()->GetTableCellRowHeaderNodeIds(); } -std::vector<int32_t> BrowserAccessibility::GetRowHeaderNodeIds( +std::vector<ui::AXNode::AXID> BrowserAccessibility::GetRowHeaderNodeIds( int row_index) const { - std::vector<int32_t> result; - node()->GetTableRowHeaderNodeIds(row_index, &result); - return result; + return node()->GetTableRowHeaderNodeIds(row_index); } ui::AXPlatformNode* BrowserAccessibility::GetTableCaption() const { @@ -1871,9 +1849,49 @@ bool BrowserAccessibility::AccessibilityPerformAction( case ax::mojom::Action::kSetScrollOffset: manager_->SetScrollOffset(*this, data.target_point); return true; - case ax::mojom::Action::kSetSelection: - manager_->SetSelection(data); + case ax::mojom::Action::kSetSelection: { + // "data.anchor_offset" and "data.focus_ofset" might need to be adjusted + // if the anchor or the focus nodes include ignored children. + ui::AXActionData selection = data; + const BrowserAccessibility* anchor_object = + manager()->GetFromID(selection.anchor_node_id); + DCHECK(anchor_object); + if (!anchor_object->PlatformIsLeaf()) { + DCHECK_GE(selection.anchor_offset, 0); + const BrowserAccessibility* anchor_child = + anchor_object->InternalGetChild(uint32_t{selection.anchor_offset}); + if (anchor_child) { + selection.anchor_offset = + int{anchor_child->node()->index_in_parent()}; + selection.anchor_node_id = anchor_child->node()->parent()->id(); + } else { + // Since the child was not found, the only alternative is that this is + // an "after children" position. + selection.anchor_offset = + int{anchor_object->node()->children().size()}; + } + } + + const BrowserAccessibility* focus_object = + manager()->GetFromID(selection.focus_node_id); + DCHECK(focus_object); + if (!focus_object->PlatformIsLeaf()) { + DCHECK_GE(selection.focus_offset, 0); + const BrowserAccessibility* focus_child = + focus_object->InternalGetChild(uint32_t{selection.focus_offset}); + if (focus_child) { + selection.focus_offset = int{focus_child->node()->index_in_parent()}; + selection.focus_node_id = focus_child->node()->parent()->id(); + } else { + // Since the child was not found, the only alternative is that this is + // an "after children" position. + selection.focus_offset = int{focus_object->node()->children().size()}; + } + } + + manager_->SetSelection(selection); return true; + } case ax::mojom::Action::kSetValue: manager_->SetValue(*this, data.value); return true; diff --git a/chromium/content/browser/accessibility/browser_accessibility.h b/chromium/content/browser/accessibility/browser_accessibility.h index c8184f66de2..df1b7977798 100644 --- a/chromium/content/browser/accessibility/browser_accessibility.h +++ b/chromium/content/browser/accessibility/browser_accessibility.h @@ -116,20 +116,11 @@ class CONTENT_EXPORT BrowserAccessibility : public ui::AXPlatformNodeDelegate { bool IsLineBreakObject() const; - // Returns true if this is a leaf node on this platform, meaning any - // children should not be exposed to this platform's native accessibility - // layer. - // The definition of a leaf may vary depending on the platform, - // but a leaf node should never have children that are focusable or - // that might send notifications. + // See AXNode::IsLeaf(). bool PlatformIsLeaf() const; - // Returns true if this is a leaf node on this platform, including - // ignored nodes, meaning any children should not be exposed to this - // platform's native accessibility layer, but a node shouldn't be - // considered a leaf node solely because it has only ignored children. - // Each platform subclass should implement this itself. - virtual bool PlatformIsLeafIncludingIgnored() const; + // See AXNode::IsLeafIncludingIgnored(). + bool PlatformIsLeafIncludingIgnored() const; // Returns true if this object can fire events. virtual bool CanFireEvents() const; @@ -143,7 +134,7 @@ class CONTENT_EXPORT BrowserAccessibility : public ui::AXPlatformNodeDelegate { virtual uint32_t PlatformChildCount() const; // Return a pointer to the child at the given index, or NULL for an - // invalid index. Returns NULL if PlatformIsLeaf() returns true. + // invalid index. Returns nullptr if PlatformIsLeaf() returns true. virtual BrowserAccessibility* PlatformGetChild(uint32_t child_index) const; BrowserAccessibility* PlatformGetParent() const; @@ -186,12 +177,6 @@ class CONTENT_EXPORT BrowserAccessibility : public ui::AXPlatformNodeDelegate { // Return a pointer to the first ancestor that is a selection container BrowserAccessibility* PlatformGetSelectionContainer() const; - // Returns true if an ancestor of this node (not including itself) is a - // leaf node, including ignored nodes, meaning that this node is not - // actually exposed to the platform, but a node shouldn't be - // considered a leaf node solely because it has only ignored children. - bool PlatformIsChildOfLeafIncludingIgnored() const; - // If this object is exposed to the platform, returns this object. Otherwise, // returns the platform leaf under which this object is found. BrowserAccessibility* PlatformGetClosestPlatformObject() const; @@ -316,7 +301,7 @@ class CONTENT_EXPORT BrowserAccessibility : public ui::AXPlatformNodeDelegate { InternalChildIterator InternalChildrenBegin() const; InternalChildIterator InternalChildrenEnd() const; - int32_t GetId() const; + ui::AXNode::AXID GetId() const; gfx::RectF GetLocation() const; ax::mojom::Role GetRole() const; int32_t GetState() const; @@ -388,30 +373,19 @@ class CONTENT_EXPORT BrowserAccessibility : public ui::AXPlatformNodeDelegate { virtual bool IsClickable() const; - // A text field is any widget in which the user should be able to enter and - // edit text. - // - // Examples include <input type="text">, <input type="password">, <textarea>, - // <div contenteditable="true">, <div role="textbox">, <div role="searchbox"> - // and <div role="combobox">. Note that when an ARIA role that indicates that - // the widget is editable is used, such as "role=textbox", the element doesn't - // need to be contenteditable for this method to return true, as in theory - // JavaScript could be used to implement editing functionality. In practice, - // this situation should be rare. + // See AXNodeData::IsTextField(). bool IsTextField() const; - // A text field that is used for entering passwords. + // See AXNodeData::IsPasswordField(). bool IsPasswordField() const; - // A text field that doesn't accept rich text content, such as text with - // special formatting or styling. + // See AXNodeData::IsPlainTextField(). bool IsPlainTextField() const; - // A text field that accepts rich text content, such as text with special - // formatting or styling. + // See AXNodeData::IsRichTextField(). bool IsRichTextField() const; - // Return true if the accessible name was explicitly set to "" by the author + // Returns true if the accessible name was explicitly set to "" by the author bool HasExplicitlyEmptyName() const; // TODO(nektar): Remove this method and replace with GetInnerText. @@ -471,6 +445,8 @@ class CONTENT_EXPORT BrowserAccessibility : public ui::AXPlatformNodeDelegate { gfx::NativeViewAccessible GetPreviousSibling() override; bool IsChildOfLeaf() const override; + bool IsChildOfPlainTextField() const override; + bool IsLeaf() const override; gfx::NativeViewAccessible GetClosestPlatformObject() const override; std::unique_ptr<ChildIterator> ChildrenBegin() override; @@ -523,10 +499,12 @@ class CONTENT_EXPORT BrowserAccessibility : public ui::AXPlatformNodeDelegate { base::Optional<int> GetTableAriaRowCount() const override; base::Optional<int> GetTableCellCount() const override; base::Optional<bool> GetTableHasColumnOrRowHeaderNode() const override; - std::vector<int32_t> GetColHeaderNodeIds() const override; - std::vector<int32_t> GetColHeaderNodeIds(int col_index) const override; - std::vector<int32_t> GetRowHeaderNodeIds() const override; - std::vector<int32_t> GetRowHeaderNodeIds(int row_index) const override; + std::vector<ui::AXNode::AXID> GetColHeaderNodeIds() const override; + std::vector<ui::AXNode::AXID> GetColHeaderNodeIds( + int col_index) const override; + std::vector<ui::AXNode::AXID> GetRowHeaderNodeIds() const override; + std::vector<ui::AXNode::AXID> GetRowHeaderNodeIds( + int row_index) const override; ui::AXPlatformNode* GetTableCaption() const override; bool IsTableRow() const override; @@ -574,6 +552,7 @@ class CONTENT_EXPORT BrowserAccessibility : public ui::AXPlatformNodeDelegate { bool IsOrderedSet() const override; base::Optional<int> GetPosInSet() const override; base::Optional<int> GetSetSize() const override; + bool IsInListMarker() const; bool IsCollapsedMenuListPopUpButton() const; BrowserAccessibility* GetCollapsedMenuListPopUpButtonAncestor() const; diff --git a/chromium/content/browser/accessibility/browser_accessibility_android.cc b/chromium/content/browser/accessibility/browser_accessibility_android.cc index 87f3bd2ddcb..c219569e5e4 100644 --- a/chromium/content/browser/accessibility/browser_accessibility_android.cc +++ b/chromium/content/browser/accessibility/browser_accessibility_android.cc @@ -116,61 +116,8 @@ base::string16 BrowserAccessibilityAndroid::GetValue() const { return value; } -bool BrowserAccessibilityAndroid::PlatformIsLeafIncludingIgnored() const { - if (BrowserAccessibility::PlatformIsLeafIncludingIgnored()) - return true; - - // Iframes are always allowed to contain children. - if (IsIframe() || GetRole() == ax::mojom::Role::kRootWebArea || - GetRole() == ax::mojom::Role::kWebArea) { - return false; - } - - // Button, date and time controls should drop their children. - switch (GetRole()) { - case ax::mojom::Role::kButton: - case ax::mojom::Role::kDate: - case ax::mojom::Role::kDateTime: - case ax::mojom::Role::kInputTime: - return true; - default: - break; - } - - // Links are never a leaf - if (IsLink()) - return false; - - // If it has a focusable child, we definitely can't leave out children. - if (HasFocusableNonOptionChild()) - return false; - - BrowserAccessibilityManagerAndroid* manager_android = - static_cast<BrowserAccessibilityManagerAndroid*>(manager()); - if (manager_android->prune_tree_for_screen_reader()) { - // Headings with text can drop their children. - base::string16 name = GetInnerText(); - if (GetRole() == ax::mojom::Role::kHeading && !name.empty()) - return true; - - // Focusable nodes with text can drop their children. - if (HasState(ax::mojom::State::kFocusable) && !name.empty()) - return true; - - // Nodes with only static text as children can drop their children. - if (HasOnlyTextChildren()) - return true; - } - - return false; -} - -bool BrowserAccessibilityAndroid::CanFireEvents() const { - return true; -} - bool BrowserAccessibilityAndroid::IsCheckable() const { - return HasIntAttribute(ax::mojom::IntAttribute::kCheckedState); + return GetData().HasCheckedState(); } bool BrowserAccessibilityAndroid::IsChecked() const { @@ -395,7 +342,7 @@ bool BrowserAccessibilityAndroid::IsInterestingOnAndroid() const { } // Otherwise, the interesting nodes are leaf nodes with non-whitespace text. - return PlatformIsLeaf() && + return IsLeaf() && !base::ContainsOnlyChars(GetInnerText(), base::kWhitespaceUTF16); } @@ -462,6 +409,67 @@ const char* BrowserAccessibilityAndroid::GetClassName() const { return ui::AXRoleToAndroidClassName(role, PlatformGetParent() != nullptr); } +bool BrowserAccessibilityAndroid::IsChildOfLeaf() const { + BrowserAccessibility* ancestor = InternalGetParent(); + + while (ancestor) { + if (ancestor->IsLeaf()) + return true; + ancestor = ancestor->InternalGetParent(); + } + + return false; +} + +bool BrowserAccessibilityAndroid::IsLeaf() const { + if (BrowserAccessibility::IsLeaf()) + return true; + + // Iframes are always allowed to contain children. + if (IsIframe() || GetRole() == ax::mojom::Role::kRootWebArea || + GetRole() == ax::mojom::Role::kWebArea) { + return false; + } + + // Button, date and time controls should drop their children. + switch (GetRole()) { + case ax::mojom::Role::kButton: + case ax::mojom::Role::kDate: + case ax::mojom::Role::kDateTime: + case ax::mojom::Role::kInputTime: + return true; + default: + break; + } + + // Links are never leaves. + if (IsLink()) + return false; + + // If it has a focusable child, we definitely can't leave out children. + if (HasFocusableNonOptionChild()) + return false; + + BrowserAccessibilityManagerAndroid* manager_android = + static_cast<BrowserAccessibilityManagerAndroid*>(manager()); + if (manager_android->prune_tree_for_screen_reader()) { + // Headings with text can drop their children. + base::string16 name = GetInnerText(); + if (GetRole() == ax::mojom::Role::kHeading && !name.empty()) + return true; + + // Focusable nodes with text can drop their children. + if (HasState(ax::mojom::State::kFocusable) && !name.empty()) + return true; + + // Nodes with only static text as children can drop their children. + if (HasOnlyTextChildren()) + return true; + } + + return false; +} + base::string16 BrowserAccessibilityAndroid::GetInnerText() const { if (IsIframe() || GetRole() == ax::mojom::Role::kWebArea) { return base::string16(); @@ -496,7 +504,7 @@ base::string16 BrowserAccessibilityAndroid::GetInnerText() const { if (GetRole() == ax::mojom::Role::kRootWebArea) return text; - // This is called from PlatformIsLeaf, so don't call PlatformChildCount + // This is called from IsLeaf, so don't call PlatformChildCount // from within this! if (text.empty() && (HasOnlyTextChildren() || (IsFocusable() && HasOnlyTextAndImageChildren()))) { @@ -1678,7 +1686,7 @@ void BrowserAccessibilityAndroid::GetWordBoundaries( } bool BrowserAccessibilityAndroid::HasFocusableNonOptionChild() const { - // This is called from PlatformIsLeaf, so don't call PlatformChildCount + // This is called from IsLeaf, so don't call PlatformChildCount // from within this! for (auto it = InternalChildrenBegin(); it != InternalChildrenEnd(); ++it) { BrowserAccessibility* child = it.get(); @@ -1709,7 +1717,7 @@ void BrowserAccessibilityAndroid::GetSuggestions( return; // TODO(accessibility): using FindTextOnlyObjectsInRange or NextInTreeOrder - // doesn't work because Android's PlatformIsLeafIncludingIgnored + // doesn't work because Android's IsLeaf // implementation deliberately excludes a lot of nodes. We need a version of // FindTextOnlyObjectsInRange and/or NextInTreeOrder that only walk // the internal tree. @@ -1790,7 +1798,7 @@ bool BrowserAccessibilityAndroid::HasImage() const { } bool BrowserAccessibilityAndroid::HasOnlyTextChildren() const { - // This is called from PlatformIsLeaf, so don't call PlatformChildCount + // This is called from IsLeaf, so don't call PlatformChildCount // from within this! for (auto it = InternalChildrenBegin(); it != InternalChildrenEnd(); ++it) { if (!it->IsTextOnlyObject()) @@ -1800,7 +1808,7 @@ bool BrowserAccessibilityAndroid::HasOnlyTextChildren() const { } bool BrowserAccessibilityAndroid::HasOnlyTextAndImageChildren() const { - // This is called from PlatformIsLeaf, so don't call PlatformChildCount + // This is called from IsLeaf, so don't call PlatformChildCount // from within this! for (auto it = InternalChildrenBegin(); it != InternalChildrenEnd(); ++it) { BrowserAccessibility* child = it.get(); diff --git a/chromium/content/browser/accessibility/browser_accessibility_android.h b/chromium/content/browser/accessibility/browser_accessibility_android.h index ed8924e9461..ee55916ca43 100644 --- a/chromium/content/browser/accessibility/browser_accessibility_android.h +++ b/chromium/content/browser/accessibility/browser_accessibility_android.h @@ -29,10 +29,6 @@ class CONTENT_EXPORT BrowserAccessibilityAndroid : public BrowserAccessibility { void OnLocationChanged() override; base::string16 GetValue() const override; - bool PlatformIsLeafIncludingIgnored() const override; - // Android needs events even on objects that are trimmed away. - bool CanFireEvents() const override; - bool IsCheckable() const; bool IsChecked() const; bool IsClickable() const override; @@ -84,6 +80,8 @@ class CONTENT_EXPORT BrowserAccessibilityAndroid : public BrowserAccessibility { bool HasImage() const; const char* GetClassName() const; + bool IsChildOfLeaf() const override; + bool IsLeaf() const override; base::string16 GetInnerText() const override; base::string16 GetHint() const; diff --git a/chromium/content/browser/accessibility/browser_accessibility_android_unittest.cc b/chromium/content/browser/accessibility/browser_accessibility_android_unittest.cc new file mode 100644 index 00000000000..00235ecfa19 --- /dev/null +++ b/chromium/content/browser/accessibility/browser_accessibility_android_unittest.cc @@ -0,0 +1,283 @@ +// Copyright (c) 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/browser/accessibility/browser_accessibility_android.h" + +#include <memory> + +#include "base/test/task_environment.h" +#include "build/build_config.h" +#include "content/browser/accessibility/browser_accessibility_manager.h" +#include "content/browser/accessibility/test_browser_accessibility_delegate.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace content { + +class BrowserAccessibilityAndroidTest : public testing::Test { + public: + BrowserAccessibilityAndroidTest(); + ~BrowserAccessibilityAndroidTest() override; + + protected: + std::unique_ptr<TestBrowserAccessibilityDelegate> + test_browser_accessibility_delegate_; + + private: + void SetUp() override; + base::test::TaskEnvironment task_environment_; + DISALLOW_COPY_AND_ASSIGN(BrowserAccessibilityAndroidTest); +}; + +BrowserAccessibilityAndroidTest::BrowserAccessibilityAndroidTest() = default; + +BrowserAccessibilityAndroidTest::~BrowserAccessibilityAndroidTest() = default; + +void BrowserAccessibilityAndroidTest::SetUp() { + ui::AXPlatformNode::NotifyAddAXModeFlags(ui::kAXModeComplete); + test_browser_accessibility_delegate_ = + std::make_unique<TestBrowserAccessibilityDelegate>(); +} + +TEST_F(BrowserAccessibilityAndroidTest, TestRetargetTextOnly) { + ui::AXNodeData text1; + text1.id = 111; + text1.role = ax::mojom::Role::kStaticText; + text1.SetName("Hello, world"); + + ui::AXNodeData para1; + para1.id = 11; + para1.role = ax::mojom::Role::kParagraph; + para1.child_ids = {text1.id}; + + ui::AXNodeData root; + root.id = 1; + root.role = ax::mojom::Role::kRootWebArea; + root.child_ids = {para1.id}; + + std::unique_ptr<BrowserAccessibilityManager> manager( + BrowserAccessibilityManager::Create( + MakeAXTreeUpdate(root, para1, text1), + test_browser_accessibility_delegate_.get())); + + BrowserAccessibility* root_obj = manager->GetRoot(); + EXPECT_FALSE(root_obj->PlatformIsLeaf()); + EXPECT_TRUE(root_obj->CanFireEvents()); + BrowserAccessibility* para_obj = root_obj->PlatformGetChild(0); + EXPECT_TRUE(para_obj->PlatformIsLeaf()); + EXPECT_TRUE(para_obj->CanFireEvents()); + BrowserAccessibility* text_obj = manager->GetFromID(111); + EXPECT_TRUE(text_obj->PlatformIsLeaf()); + EXPECT_FALSE(text_obj->CanFireEvents()); + BrowserAccessibility* updated = manager->RetargetForEvents( + text_obj, BrowserAccessibilityManager::RetargetEventType:: + RetargetEventTypeBlinkHover); + // |updated| should be the paragraph. + EXPECT_EQ(11, updated->GetId()); + EXPECT_TRUE(updated->CanFireEvents()); + manager.reset(); +} + +TEST_F(BrowserAccessibilityAndroidTest, TestRetargetHeading) { + ui::AXNodeData text1; + text1.id = 111; + text1.role = ax::mojom::Role::kStaticText; + + ui::AXNodeData heading1; + heading1.id = 11; + heading1.role = ax::mojom::Role::kHeading; + heading1.SetName("heading"); + heading1.child_ids = {text1.id}; + + ui::AXNodeData root; + root.id = 1; + root.role = ax::mojom::Role::kRootWebArea; + root.child_ids = {heading1.id}; + + std::unique_ptr<BrowserAccessibilityManager> manager( + BrowserAccessibilityManager::Create( + MakeAXTreeUpdate(root, heading1, text1), + test_browser_accessibility_delegate_.get())); + + BrowserAccessibility* root_obj = manager->GetRoot(); + EXPECT_FALSE(root_obj->PlatformIsLeaf()); + EXPECT_TRUE(root_obj->CanFireEvents()); + BrowserAccessibility* heading_obj = root_obj->PlatformGetChild(0); + EXPECT_TRUE(heading_obj->PlatformIsLeaf()); + EXPECT_TRUE(heading_obj->CanFireEvents()); + BrowserAccessibility* text_obj = manager->GetFromID(111); + EXPECT_TRUE(text_obj->PlatformIsLeaf()); + EXPECT_FALSE(text_obj->CanFireEvents()); + BrowserAccessibility* updated = manager->RetargetForEvents( + text_obj, BrowserAccessibilityManager::RetargetEventType:: + RetargetEventTypeBlinkHover); + // |updated| should be the heading. + EXPECT_EQ(11, updated->GetId()); + EXPECT_TRUE(updated->CanFireEvents()); + manager.reset(); +} + +TEST_F(BrowserAccessibilityAndroidTest, TestRetargetFocusable) { + ui::AXNodeData text1; + text1.id = 111; + text1.role = ax::mojom::Role::kStaticText; + + ui::AXNodeData para1; + para1.id = 11; + para1.role = ax::mojom::Role::kParagraph; + para1.AddState(ax::mojom::State::kFocusable); + para1.SetName("focusable"); + para1.child_ids = {text1.id}; + + ui::AXNodeData root; + root.id = 1; + root.role = ax::mojom::Role::kRootWebArea; + root.child_ids = {para1.id}; + + std::unique_ptr<BrowserAccessibilityManager> manager( + BrowserAccessibilityManager::Create( + MakeAXTreeUpdate(root, para1, text1), + test_browser_accessibility_delegate_.get())); + + BrowserAccessibility* root_obj = manager->GetRoot(); + EXPECT_FALSE(root_obj->PlatformIsLeaf()); + EXPECT_TRUE(root_obj->CanFireEvents()); + BrowserAccessibility* para_obj = root_obj->PlatformGetChild(0); + EXPECT_TRUE(para_obj->PlatformIsLeaf()); + EXPECT_TRUE(para_obj->CanFireEvents()); + BrowserAccessibility* text_obj = manager->GetFromID(111); + EXPECT_TRUE(text_obj->PlatformIsLeaf()); + EXPECT_FALSE(text_obj->CanFireEvents()); + BrowserAccessibility* updated = manager->RetargetForEvents( + text_obj, BrowserAccessibilityManager::RetargetEventType:: + RetargetEventTypeBlinkHover); + // |updated| should be the paragraph. + EXPECT_EQ(11, updated->GetId()); + EXPECT_TRUE(updated->CanFireEvents()); + manager.reset(); +} + +TEST_F(BrowserAccessibilityAndroidTest, TestRetargetInputControl) { + // Build the tree that has a form with input time. + // +rootWebArea + // ++genericContainer + // +++form + // ++++labelText + // +++++staticText + // ++++inputTime + // +++++genericContainer + // ++++++staticText + // ++++button + // +++++staticText + ui::AXNodeData label_text; + label_text.id = 11111; + label_text.role = ax::mojom::Role::kStaticText; + label_text.SetName("label"); + + ui::AXNodeData label; + label.id = 1111; + label.role = ax::mojom::Role::kLabelText; + label.child_ids = {label_text.id}; + + ui::AXNodeData input_text; + input_text.id = 111211; + input_text.role = ax::mojom::Role::kStaticText; + input_text.SetName("input_text"); + + ui::AXNodeData input_container; + input_container.id = 11121; + input_container.role = ax::mojom::Role::kGenericContainer; + input_container.child_ids = {input_text.id}; + + ui::AXNodeData input_time; + input_time.id = 1112; + input_time.role = ax::mojom::Role::kInputTime; + input_time.AddState(ax::mojom::State::kFocusable); + input_time.child_ids = {input_container.id}; + + ui::AXNodeData button_text; + button_text.id = 11131; + button_text.role = ax::mojom::Role::kStaticText; + button_text.AddState(ax::mojom::State::kFocusable); + button_text.SetName("button"); + + ui::AXNodeData button; + button.id = 1113; + button.role = ax::mojom::Role::kButton; + button.child_ids = {button_text.id}; + + ui::AXNodeData form; + form.id = 111; + form.role = ax::mojom::Role::kForm; + form.child_ids = {label.id, input_time.id, button.id}; + + ui::AXNodeData container; + container.id = 11; + container.role = ax::mojom::Role::kGenericContainer; + container.child_ids = {form.id}; + + ui::AXNodeData root; + root.id = 1; + root.role = ax::mojom::Role::kRootWebArea; + root.child_ids = {container.id}; + + std::unique_ptr<BrowserAccessibilityManager> manager( + BrowserAccessibilityManager::Create( + MakeAXTreeUpdate(root, container, form, label, label_text, input_time, + input_container, input_text, button, button_text), + test_browser_accessibility_delegate_.get())); + + BrowserAccessibility* root_obj = manager->GetRoot(); + EXPECT_FALSE(root_obj->PlatformIsLeaf()); + EXPECT_TRUE(root_obj->CanFireEvents()); + BrowserAccessibility* label_obj = manager->GetFromID(1111); + EXPECT_TRUE(label_obj->PlatformIsLeaf()); + EXPECT_TRUE(label_obj->CanFireEvents()); + BrowserAccessibility* label_text_obj = manager->GetFromID(11111); + EXPECT_TRUE(label_text_obj->PlatformIsLeaf()); + EXPECT_FALSE(label_text_obj->CanFireEvents()); + BrowserAccessibility* updated = manager->RetargetForEvents( + label_text_obj, BrowserAccessibilityManager::RetargetEventType:: + RetargetEventTypeBlinkHover); + // |updated| should be the labelText. + EXPECT_EQ(1111, updated->GetId()); + EXPECT_TRUE(updated->CanFireEvents()); + + BrowserAccessibility* input_time_obj = manager->GetFromID(1112); + EXPECT_TRUE(input_time_obj->PlatformIsLeaf()); + EXPECT_TRUE(input_time_obj->CanFireEvents()); + BrowserAccessibility* input_time_container_obj = manager->GetFromID(11121); + EXPECT_TRUE(input_time_container_obj->PlatformIsLeaf()); + EXPECT_FALSE(input_time_container_obj->CanFireEvents()); + updated = manager->RetargetForEvents( + input_time_container_obj, BrowserAccessibilityManager::RetargetEventType:: + RetargetEventTypeBlinkHover); + // |updated| should be the inputTime. + EXPECT_EQ(1112, updated->GetId()); + EXPECT_TRUE(updated->CanFireEvents()); + BrowserAccessibility* input_text_obj = manager->GetFromID(111211); + EXPECT_TRUE(input_text_obj->PlatformIsLeaf()); + EXPECT_FALSE(input_text_obj->CanFireEvents()); + updated = manager->RetargetForEvents( + input_text_obj, BrowserAccessibilityManager::RetargetEventType:: + RetargetEventTypeBlinkHover); + // |updated| should be the inputTime. + EXPECT_EQ(1112, updated->GetId()); + EXPECT_TRUE(updated->CanFireEvents()); + + BrowserAccessibility* button_obj = manager->GetFromID(1113); + EXPECT_TRUE(button_obj->PlatformIsLeaf()); + EXPECT_TRUE(button_obj->CanFireEvents()); + BrowserAccessibility* button_text_obj = manager->GetFromID(11131); + EXPECT_TRUE(button_text_obj->PlatformIsLeaf()); + EXPECT_FALSE(button_text_obj->CanFireEvents()); + updated = manager->RetargetForEvents( + button_text_obj, BrowserAccessibilityManager::RetargetEventType:: + RetargetEventTypeBlinkHover); + // |updated| should be the button. + EXPECT_EQ(1113, updated->GetId()); + EXPECT_TRUE(updated->CanFireEvents()); + manager.reset(); +} + +} // namespace content diff --git a/chromium/content/browser/accessibility/browser_accessibility_auralinux_unittest.cc b/chromium/content/browser/accessibility/browser_accessibility_auralinux_unittest.cc index 5e588edf716..e3f0043c295 100644 --- a/chromium/content/browser/accessibility/browser_accessibility_auralinux_unittest.cc +++ b/chromium/content/browser/accessibility/browser_accessibility_auralinux_unittest.cc @@ -179,33 +179,31 @@ TEST_F(BrowserAccessibilityAuraLinuxTest, TestComplexHypertext) { check_box.role = ax::mojom::Role::kCheckBox; check_box.SetCheckedState(ax::mojom::CheckedState::kTrue); check_box.SetName(check_box_name); + // ARIA checkbox where the name is derived from its inner text. + check_box.SetNameFrom(ax::mojom::NameFrom::kContents); check_box.SetValue(check_box_value); ui::AXNodeData radio_button, radio_button_text; radio_button.id = 15; radio_button_text.id = 17; - radio_button_text.SetName(radio_button_text_name); radio_button.role = ax::mojom::Role::kRadioButton; radio_button_text.role = ax::mojom::Role::kStaticText; + radio_button_text.SetName(radio_button_text_name); radio_button.child_ids.push_back(radio_button_text.id); ui::AXNodeData link, link_text; link.id = 16; link_text.id = 18; - link_text.SetName(link_text_name); link.role = ax::mojom::Role::kLink; link_text.role = ax::mojom::Role::kStaticText; + link_text.SetName(link_text_name); link.child_ids.push_back(link_text.id); ui::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(radio_button.id); - root.child_ids.push_back(link.id); + root.child_ids = {text1.id, combo_box.id, text2.id, + check_box.id, radio_button.id, link.id}; std::unique_ptr<BrowserAccessibilityManager> manager( BrowserAccessibilityManager::Create( @@ -355,6 +353,7 @@ TEST_F(BrowserAccessibilityAuraLinuxTest, link.AddState(ax::mojom::State::kFocusable); link.AddState(ax::mojom::State::kLinked); link.SetName("lnk"); + link.SetNameFrom(ax::mojom::NameFrom::kContents); link.AddTextStyle(ax::mojom::TextStyle::kUnderline); ui::AXNodeData link_text; diff --git a/chromium/content/browser/accessibility/browser_accessibility_cocoa.h b/chromium/content/browser/accessibility/browser_accessibility_cocoa.h index 5124b17f020..947a5482780 100644 --- a/chromium/content/browser/accessibility/browser_accessibility_cocoa.h +++ b/chromium/content/browser/accessibility/browser_accessibility_cocoa.h @@ -28,6 +28,23 @@ struct AXTextEdit { base::string16 deleted_text; }; +// Returns true if the given object is AXTextMarker object. +bool IsAXTextMarker(id); + +// Returns true if the given object is AXTextMarkerRange object. +bool IsAXTextMarkerRange(id); + +// Returns browser accessibility position for the given AXTextMarker. +BrowserAccessibilityPosition::AXPositionInstance AXTextMarkerToPosition(id); + +// Returns browser accessibility range for the given AXTextMarkerRange. +BrowserAccessibilityPosition::AXRangeType AXTextMarkerRangeToRange(id); + +// Returns AXTextMarker for the given browser accessibility position. +id AXTextMarkerFrom(const BrowserAccessibilityCocoa* anchor, + int offset, + ax::mojom::TextAffinity affinity); + } // namespace content // BrowserAccessibilityCocoa is a cocoa wrapper around the BrowserAccessibility diff --git a/chromium/content/browser/accessibility/browser_accessibility_cocoa.mm b/chromium/content/browser/accessibility/browser_accessibility_cocoa.mm index dc282c8b9a7..8e2592948d6 100644 --- a/chromium/content/browser/accessibility/browser_accessibility_cocoa.mm +++ b/chromium/content/browser/accessibility/browser_accessibility_cocoa.mm @@ -301,11 +301,10 @@ id CreateTextMarkerRange(const AXPlatformRange range) { BrowserAccessibilityPositionInstance CreatePositionFromTextMarker( id text_marker) { - AXTextMarkerRef cf_text_marker = static_cast<AXTextMarkerRef>(text_marker); - DCHECK(cf_text_marker); - if (CFGetTypeID(cf_text_marker) != AXTextMarkerGetTypeID()) + if (!content::IsAXTextMarker(text_marker)) return BrowserAccessibilityPosition::CreateNullPosition(); + AXTextMarkerRef cf_text_marker = static_cast<AXTextMarkerRef>(text_marker); if (AXTextMarkerGetLength(cf_text_marker) != sizeof(SerializedPosition)) return BrowserAccessibilityPosition::CreateNullPosition(); @@ -318,11 +317,12 @@ BrowserAccessibilityPositionInstance CreatePositionFromTextMarker( } AXPlatformRange CreateRangeFromTextMarkerRange(id marker_range) { + if (!content::IsAXTextMarkerRange(marker_range)) { + return AXPlatformRange(); + } + AXTextMarkerRangeRef cf_marker_range = static_cast<AXTextMarkerRangeRef>(marker_range); - DCHECK(cf_marker_range); - if (CFGetTypeID(cf_marker_range) != AXTextMarkerRangeGetTypeID()) - return AXPlatformRange(); base::ScopedCFTypeRef<AXTextMarkerRef> start_marker( AXTextMarkerRangeCopyStartMarker(cf_marker_range)); @@ -727,6 +727,44 @@ NSString* const NSAccessibilityRequiredAttributeChrome = @"AXRequired"; #define NSAccessibilityLanguageAttribute @"AXLanguage" #endif +bool content::IsAXTextMarker(id object) { + if (object == nil) + return false; + + AXTextMarkerRef cf_text_marker = static_cast<AXTextMarkerRef>(object); + DCHECK(cf_text_marker); + return CFGetTypeID(cf_text_marker) == AXTextMarkerGetTypeID(); +} + +bool content::IsAXTextMarkerRange(id object) { + if (object == nil) + return false; + + AXTextMarkerRangeRef cf_marker_range = + static_cast<AXTextMarkerRangeRef>(object); + DCHECK(cf_marker_range); + return CFGetTypeID(cf_marker_range) == AXTextMarkerRangeGetTypeID(); +} + +BrowserAccessibilityPosition::AXPositionInstance +content::AXTextMarkerToPosition(id text_marker) { + return CreatePositionFromTextMarker(text_marker); +} + +BrowserAccessibilityPosition::AXRangeType +content::AXTextMarkerRangeToRange(id text_marker_range) { + return CreateRangeFromTextMarkerRange(text_marker_range); +} + +id content::AXTextMarkerFrom(const BrowserAccessibilityCocoa* anchor, + int offset, + ax::mojom::TextAffinity affinity) { + BrowserAccessibility* anchor_node = [anchor owner]; + BrowserAccessibilityPositionInstance position = + CreateTextPosition(*anchor_node, offset, affinity); + return CreateTextMarker(std::move(position)); +} + @implementation BrowserAccessibilityCocoa + (void)initialize { @@ -1032,13 +1070,7 @@ NSString* const NSAccessibilityRequiredAttributeChrome = @"AXRequired"; NSMutableArray* ret = [[[NSMutableArray alloc] init] autorelease]; if (is_table_like) { // If this is a table, return all column headers. - std::set<int32_t> headerIds; - for (int i = 0; i < *_owner->GetTableColCount(); i++) { - std::vector<int32_t> colHeaderIds = table->GetColHeaderNodeIds(i); - std::copy(colHeaderIds.begin(), colHeaderIds.end(), - std::inserter(headerIds, headerIds.end())); - } - for (int32_t id : headerIds) { + for (int32_t id : table->GetColHeaderNodeIds()) { BrowserAccessibility* cell = _owner->manager()->GetFromID(id); if (cell) [ret addObject:ToBrowserAccessibilityCocoa(cell)]; @@ -1246,7 +1278,8 @@ NSString* const NSAccessibilityRequiredAttributeChrome = @"AXRequired"; NSMutableArray* ret = [[[NSMutableArray alloc] init] autorelease]; std::string classes; - if (_owner->GetHtmlAttribute("class", &classes)) { + if (_owner->GetStringAttribute(ax::mojom::StringAttribute::kClassName, + &classes)) { std::vector<std::string> split_classes = base::SplitString( classes, " ", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); for (const auto& className : split_classes) @@ -1824,6 +1857,23 @@ NSString* const NSAccessibilityRequiredAttributeChrome = @"AXRequired"; base::string16 deletedText = oldValue.substr(i, oldValue.length() - i - j); base::string16 insertedText = newValue.substr(i, newValue.length() - i - j); + + // Heuristic for editable combobox. If more than 1 character is inserted or + // deleted, and the caret is at the end of the field, assume the entire text + // field changed. + // TODO(nektar) Remove this once editing intents are implemented, + // and the actual inserted and deleted text is passed over from Blink. + if ([self internalRole] == ax::mojom::Role::kTextFieldWithComboBox && + (deletedText.length() > 1 || insertedText.length() > 1)) { + int sel_start, sel_end; + _owner->GetIntAttribute(ax::mojom::IntAttribute::kTextSelStart, &sel_start); + _owner->GetIntAttribute(ax::mojom::IntAttribute::kTextSelEnd, &sel_end); + if (size_t{sel_start} == newValue.length() && + size_t{sel_end} == newValue.length()) { + // Don't include oldValue as it would be announced -- very confusing. + return content::AXTextEdit(newValue, base::string16()); + } + } return content::AXTextEdit(insertedText, deletedText); } @@ -2055,9 +2105,7 @@ NSString* const NSAccessibilityRequiredAttributeChrome = @"AXRequired"; } } else { // Otherwise this is a cell, return the row headers for this cell. - std::vector<int32_t> rowHeaderIds; - _owner->node()->GetTableCellRowHeaderNodeIds(&rowHeaderIds); - for (int32_t id : rowHeaderIds) { + for (int32_t id : _owner->node()->GetTableCellRowHeaderNodeIds()) { BrowserAccessibility* cell = _owner->manager()->GetFromID(id); if (cell) [ret addObject:ToBrowserAccessibilityCocoa(cell)]; @@ -2388,12 +2436,9 @@ NSString* const NSAccessibilityRequiredAttributeChrome = @"AXRequired"; } else if ([role isEqualToString:NSAccessibilityButtonRole]) { // AXValue does not make sense for pure buttons. return @""; - } else if (_owner->HasIntAttribute(ax::mojom::IntAttribute::kCheckedState) || - [role isEqualToString:NSAccessibilityRadioButtonRole]) { - // On Mac, tabs are exposed as radio buttons, and are treated as checkable. + } else if ([self isCheckable]) { int value; - const auto checkedState = static_cast<ax::mojom::CheckedState>( - _owner->GetIntAttribute(ax::mojom::IntAttribute::kCheckedState)); + const auto checkedState = _owner->GetData().GetCheckedState(); switch (checkedState) { case ax::mojom::CheckedState::kTrue: value = 1; @@ -2464,11 +2509,8 @@ NSString* const NSAccessibilityRequiredAttributeChrome = @"AXRequired"; if (![self instanceActive]) return nil; - std::vector<int32_t> unique_cell_ids; - _owner->node()->GetTableUniqueCellIds(&unique_cell_ids); NSMutableArray* ret = [[[NSMutableArray alloc] init] autorelease]; - for (size_t i = 0; i < unique_cell_ids.size(); ++i) { - int id = unique_cell_ids[i]; + for (int32_t id : _owner->node()->GetTableUniqueCellIds()) { BrowserAccessibility* cell = _owner->manager()->GetFromID(id); if (cell) [ret addObject:ToBrowserAccessibilityCocoa(cell)]; @@ -3568,6 +3610,14 @@ NSString* const NSAccessibilityRequiredAttributeChrome = @"AXRequired"; return [self isIgnored]; } +- (BOOL)isCheckable { + if (![self instanceActive]) + return NO; + + return _owner->GetData().HasCheckedState() || + _owner->GetData().role == ax::mojom::Role::kTab; +} + // Performs the given accessibility action on the webkit accessibility object // that backs this object. - (void)accessibilityPerformAction:(NSString*)action { @@ -3583,7 +3633,7 @@ NSString* const NSAccessibilityRequiredAttributeChrome = @"AXRequired"; if ([action isEqualToString:NSAccessibilityPressAction]) { manager->DoDefaultAction(*_owner); if (_owner->GetData().GetRestriction() != ax::mojom::Restriction::kNone || - !_owner->HasIntAttribute(ax::mojom::IntAttribute::kCheckedState)) + ![self isCheckable]) return; // Hack: preemptively set the checked state to what it should become, // otherwise VoiceOver will very likely report the old, incorrect state to @@ -3679,13 +3729,15 @@ NSString* const NSAccessibilityRequiredAttributeChrome = @"AXRequired"; if (![self instanceActive]) return nil; + // The point we receive is in frame coordinates. + // Convert to screen coordinates and then to physical pixel coordinates. BrowserAccessibilityManager* manager = _owner->manager(); gfx::Point screen_point(point.x, point.y); screen_point += manager->GetViewBoundsInScreenCoordinates().OffsetFromOrigin(); gfx::Point physical_pixel_point = - content::IsUseZoomForDSFEnabled() + IsUseZoomForDSFEnabled() ? screen_point : ScaleToRoundedPoint(screen_point, manager->device_scale_factor()); @@ -3711,9 +3763,8 @@ NSString* const NSAccessibilityRequiredAttributeChrome = @"AXRequired"; } - (BOOL)accessibilityNotifiesWhenDestroyed { - TRACE_EVENT1("accessibility", - "BrowserAccessibilityCocoa::accessibilityNotifiesWhenDestroyed", - "role=", ui::ToString([self internalRole])); + TRACE_EVENT0("accessibility", + "BrowserAccessibilityCocoa::accessibilityNotifiesWhenDestroyed"); // Indicate that BrowserAccessibilityCocoa will post a notification when it's // destroyed (see -detach). This allows VoiceOver to do some internal things // more efficiently. diff --git a/chromium/content/browser/accessibility/browser_accessibility_com_win.cc b/chromium/content/browser/accessibility/browser_accessibility_com_win.cc index 89fd920e1cb..a8a043f82b4 100644 --- a/chromium/content/browser/accessibility/browser_accessibility_com_win.cc +++ b/chromium/content/browser/accessibility/browser_accessibility_com_win.cc @@ -318,9 +318,8 @@ IFACEMETHODIMP BrowserAccessibilityComWin::get_newText( if (!new_text) return E_INVALIDARG; - if (!old_win_attributes_ && !force_new_hypertext_) + if (!old_win_attributes_) return E_FAIL; - force_new_hypertext_ = false; size_t start, old_len, new_len; ComputeHypertextRemovedAndInserted(&start, &old_len, &new_len); @@ -446,18 +445,20 @@ IFACEMETHODIMP BrowserAccessibilityComWin::get_attributes( *start_offset = FindStartOfStyle(offset, ax::mojom::MoveDirection::kBackward); *end_offset = FindStartOfStyle(offset, ax::mojom::MoveDirection::kForward); - const ui::TextAttributeList& attributes = - offset_to_text_attributes().find(*start_offset)->second; - std::ostringstream attributes_stream; - for (const ui::TextAttribute& attribute : attributes) { - // Don't expose the default language value of "en-US". - // TODO(nektar): Determine if it's possible to check against the interface - // language. - if (attribute.first == "language" && attribute.second == "en-US") - continue; - - attributes_stream << attribute.first << ":" << attribute.second << ";"; + auto iter = offset_to_text_attributes().find(*start_offset); + if (iter != offset_to_text_attributes().end()) { + const ui::TextAttributeList& attributes = iter->second; + + for (const ui::TextAttribute& attribute : attributes) { + // Don't expose the default language value of "en-US". + // TODO(nektar): Determine if it's possible to check against the interface + // language. + if (attribute.first == "language" && attribute.second == "en-US") + continue; + + attributes_stream << attribute.first << ":" << attribute.second << ";"; + } } base::string16 attributes_str = base::UTF8ToUTF16(attributes_stream.str()); @@ -1481,7 +1482,11 @@ void BrowserAccessibilityComWin::UpdateStep3FireEvents() { } // Fire hypertext-related events. - if (ShouldFireHypertextEvents()) { + // Do not fire removed/inserted when a name change event will be fired by + // AXEventGenerator, as they are providing redundant information and will + // lead to duplicate announcements. + if (name() == old_win_attributes_->name || + GetData().GetNameFrom() == ax::mojom::NameFrom::kContents) { size_t start, old_len, new_len; ComputeHypertextRemovedAndInserted(&start, &old_len, &new_len); if (old_len > 0) { @@ -1501,22 +1506,6 @@ void BrowserAccessibilityComWin::UpdateStep3FireEvents() { old_hypertext_ = ui::AXHypertext(); } -bool BrowserAccessibilityComWin::ShouldFireHypertextEvents() const { - // Do not fire removed/inserted when a name change event will be fired by - // AXEventGenerator, as they are providing redundant information and will - // lead to duplicate announcements. - if (name() != old_win_attributes_->name && - GetData().GetNameFrom() != ax::mojom::NameFrom::kContents) - return false; - - // Similarly, for changes to live-regions we already fire an inserted event in - // BrowserAccessibilityManagerWin, so we don't want an extra event here. - if (GetData().IsContainedInActiveLiveRegion()) - return false; - - return true; -} - BrowserAccessibilityManager* BrowserAccessibilityComWin::Manager() const { DCHECK(owner()); diff --git a/chromium/content/browser/accessibility/browser_accessibility_com_win.h b/chromium/content/browser/accessibility/browser_accessibility_com_win.h index f67b0f5aa29..f839748e4e5 100644 --- a/chromium/content/browser/accessibility/browser_accessibility_com_win.h +++ b/chromium/content/browser/accessibility/browser_accessibility_com_win.h @@ -356,9 +356,6 @@ class __declspec(uuid("562072fe-3390-43b1-9e2c-dd4118f5ac79")) BrowserAccessibilityManager* Manager() const; - // Private helper methods. - bool ShouldFireHypertextEvents() const; - // // AXPlatformNode overrides // diff --git a/chromium/content/browser/accessibility/browser_accessibility_mac_unittest.mm b/chromium/content/browser/accessibility/browser_accessibility_mac_unittest.mm index d9be35fdbda..4a417e2929f 100644 --- a/chromium/content/browser/accessibility/browser_accessibility_mac_unittest.mm +++ b/chromium/content/browser/accessibility/browser_accessibility_mac_unittest.mm @@ -90,10 +90,10 @@ class BrowserAccessibilityMacTest : public ui::CocoaTest { ui::AXNodeData child1; child1.id = 1001; + child1.role = ax::mojom::Role::kButton; child1.SetName("Child1"); child1.relative_bounds.bounds.set_width(250); child1.relative_bounds.bounds.set_height(100); - child1.role = ax::mojom::Role::kButton; ui::AXNodeData child2; child2.id = 1002; diff --git a/chromium/content/browser/accessibility/browser_accessibility_manager.cc b/chromium/content/browser/accessibility/browser_accessibility_manager.cc index f22af5da7db..739a19d13eb 100644 --- a/chromium/content/browser/accessibility/browser_accessibility_manager.cc +++ b/chromium/content/browser/accessibility/browser_accessibility_manager.cc @@ -284,6 +284,12 @@ bool BrowserAccessibilityManager::CanFireEvents() const { return true; } +BrowserAccessibility* BrowserAccessibilityManager::RetargetForEvents( + BrowserAccessibility* node, + RetargetEventType type) const { + return node; +} + void BrowserAccessibilityManager::FireFocusEvent(BrowserAccessibility* node) { if (g_focus_change_callback_for_testing.Get()) g_focus_change_callback_for_testing.Get().Run(); @@ -324,7 +330,8 @@ BrowserAccessibility* BrowserAccessibilityManager::GetParentNodeFromParentTree() ui::AXTreeID parent_tree_id = GetParentTreeID(); BrowserAccessibilityManager* parent_manager = BrowserAccessibilityManager::FromID(parent_tree_id); - return parent ? parent_manager->GetFromAXNode(parent) : nullptr; + return parent && parent_manager ? parent_manager->GetFromAXNode(parent) + : nullptr; } BrowserAccessibility* BrowserAccessibilityManager::GetPopupRoot() const { @@ -438,6 +445,8 @@ bool BrowserAccessibilityManager::OnAccessibilityEvents( if (!connected_to_parent_tree_node_) { parent->OnDataChanged(); parent->UpdatePlatformAttributes(); + parent = RetargetForEvents(parent, + RetargetEventType::RetargetEventTypeGenerated); FireGeneratedEvent(ui::AXEventGenerator::Event::CHILDREN_CHANGED, parent); connected_to_parent_tree_node_ = true; } @@ -463,6 +472,11 @@ bool BrowserAccessibilityManager::OnAccessibilityEvents( // Fire any events related to changes to the tree. for (const auto& targeted_event : event_generator()) { BrowserAccessibility* event_target = GetFromAXNode(targeted_event.node); + if (!event_target) + continue; + + event_target = RetargetForEvents( + event_target, RetargetEventType::RetargetEventTypeGenerated); if (!event_target || !event_target->CanFireEvents()) continue; @@ -479,13 +493,20 @@ bool BrowserAccessibilityManager::OnAccessibilityEvents( for (const ui::AXEvent& event : details.events) { // Fire the native event. BrowserAccessibility* event_target = GetFromID(event.id); - if (!event_target || !event_target->CanFireEvents()) + if (!event_target) + continue; + RetargetEventType type = + event.event_type == ax::mojom::Event::kHover + ? RetargetEventType::RetargetEventTypeBlinkHover + : RetargetEventType::RetargetEventTypeBlinkGeneral; + BrowserAccessibility* retargeted = RetargetForEvents(event_target, type); + if (!retargeted || !retargeted->CanFireEvents()) continue; if (root_manager && event.event_type == ax::mojom::Event::kHover) root_manager->CacheHitTestResult(event_target); - FireBlinkEvent(event.event_type, event_target); + FireBlinkEvent(event.event_type, retargeted); } if (received_load_complete_event) { @@ -896,11 +917,7 @@ void BrowserAccessibilityManager::HitTest(const gfx::Point& frame_point) const { if (!delegate_) return; - ui::AXActionData action_data; - action_data.action = ax::mojom::Action::kHitTest; - action_data.target_point = frame_point; - action_data.hit_test_event_to_fire = ax::mojom::Event::kHover; - delegate_->AccessibilityPerformAction(action_data); + delegate_->AccessibilityHitTest(frame_point, ax::mojom::Event::kHover, 0, {}); } gfx::Rect BrowserAccessibilityManager::GetViewBoundsInScreenCoordinates() @@ -1560,7 +1577,8 @@ void BrowserAccessibilityManager::CacheHitTestResult( } void BrowserAccessibilityManager::DidActivatePortal( - WebContents* predecessor_contents) { + WebContents* predecessor_contents, + base::TimeTicks activation_time) { if (GetTreeData().loaded) { FireGeneratedEvent(ui::AXEventGenerator::Event::PORTAL_ACTIVATED, GetRoot()); @@ -1595,7 +1613,8 @@ void BrowserAccessibilityManager::CollectChangedNodesAndParentsForAtomicUpdate( if (!parent) continue; - if (ui::IsTextOrLineBreak(changed_node->data().role)) { + if (changed_node->IsText() && + changed_node->data().role != ax::mojom::Role::kInlineTextBox) { BrowserAccessibility* parent_obj = GetFromAXNode(parent); if (parent_obj) nodes_needing_update->insert(parent_obj->GetAXPlatformNode()); @@ -1618,6 +1637,7 @@ void BrowserAccessibilityManager::CollectChangedNodesAndParentsForAtomicUpdate( bool BrowserAccessibilityManager::ShouldFireEventForNode( BrowserAccessibility* node) const { + node = RetargetForEvents(node, RetargetEventType::RetargetEventTypeGenerated); if (!node || !node->CanFireEvents()) return false; diff --git a/chromium/content/browser/accessibility/browser_accessibility_manager.h b/chromium/content/browser/accessibility/browser_accessibility_manager.h index 8892004aaac..809ca3a2ad2 100644 --- a/chromium/content/browser/accessibility/browser_accessibility_manager.h +++ b/chromium/content/browser/accessibility/browser_accessibility_manager.h @@ -96,6 +96,12 @@ class CONTENT_EXPORT BrowserAccessibilityDelegate { virtual gfx::NativeViewAccessible AccessibilityGetNativeViewAccessibleForWindow() = 0; virtual WebContents* AccessibilityWebContents() = 0; + virtual void AccessibilityHitTest( + const gfx::Point& point_in_frame_pixels, + ax::mojom::Event opt_event_to_fire, + int opt_request_id, + base::OnceCallback<void(BrowserAccessibilityManager* hit_manager, + int hit_node_id)> opt_callback) = 0; // Returns true if this delegate represents the main (topmost) frame in a // tree of frames. @@ -147,6 +153,17 @@ class CONTENT_EXPORT BrowserAccessibilityManager : public ui::AXTreeObserver, static ui::AXTreeUpdate GetEmptyDocument(); + enum RetargetEventType { + RetargetEventTypeGenerated = 0, + RetargetEventTypeBlinkGeneral, + RetargetEventTypeBlinkHover, + }; + + // Return |node| by default, but some platforms want to update the target node + // based on the event type. + virtual BrowserAccessibility* RetargetForEvents(BrowserAccessibility* node, + RetargetEventType type) const; + // Subclasses override these methods to send native event notifications. virtual void FireFocusEvent(BrowserAccessibility* node); virtual void FireBlinkEvent(ax::mojom::Event event_type, @@ -202,7 +219,8 @@ class CONTENT_EXPORT BrowserAccessibilityManager : public ui::AXTreeObserver, // WebContentsObserver overrides void DidStopLoading() override; - void DidActivatePortal(WebContents* predecessor_contents) override; + void DidActivatePortal(WebContents* predecessor_contents, + base::TimeTicks activation_time) override; // Keep track of if this page is hidden by an interstitial, in which case // we need to suppress all events. diff --git a/chromium/content/browser/accessibility/browser_accessibility_manager_android.cc b/chromium/content/browser/accessibility/browser_accessibility_manager_android.cc index 7ac1ed3294c..d52cec13ea1 100644 --- a/chromium/content/browser/accessibility/browser_accessibility_manager_android.cc +++ b/chromium/content/browser/accessibility/browser_accessibility_manager_android.cc @@ -72,6 +72,55 @@ BrowserAccessibility* BrowserAccessibilityManagerAndroid::GetFocus() const { return focus; } +BrowserAccessibility* BrowserAccessibilityManagerAndroid::RetargetForEvents( + BrowserAccessibility* node, + RetargetEventType type) const { + // Sometimes we get events on nodes in our internal accessibility tree + // that aren't exposed on Android. Get |updated| to point to the highest + // ancestor that's a leaf node. + BrowserAccessibility* updated = node->PlatformGetClosestPlatformObject(); + + switch (type) { + case RetargetEventType::RetargetEventTypeGenerated: { + // If the closest platform object is a password field, the event we're + // getting is doing something in the shadow dom, for example replacing a + // character with a dot after a short pause. On Android we don't want to + // fire an event for those changes, but we do want to make sure our + // internal state is correct, so we call OnDataChanged() and then return. + if (updated->IsPasswordField() && node != updated) { + updated->OnDataChanged(); + return nullptr; + } + break; + } + case RetargetEventType::RetargetEventTypeBlinkGeneral: + break; + case RetargetEventType::RetargetEventTypeBlinkHover: { + // If this node is uninteresting and just a wrapper around a sole + // interesting descendant, prefer that descendant instead. + const BrowserAccessibilityAndroid* android_node = + static_cast<BrowserAccessibilityAndroid*>(updated); + const BrowserAccessibilityAndroid* sole_interesting_node = + android_node->GetSoleInterestingNodeFromSubtree(); + if (sole_interesting_node) + android_node = sole_interesting_node; + + // Finally, if this node is still uninteresting, try to walk up to + // find an interesting parent. + while (android_node && !android_node->IsInterestingOnAndroid()) { + android_node = static_cast<BrowserAccessibilityAndroid*>( + android_node->PlatformGetParent()); + } + updated = const_cast<BrowserAccessibilityAndroid*>(android_node); + break; + } + default: + NOTREACHED(); + break; + } + return updated; +} + void BrowserAccessibilityManagerAndroid::FireFocusEvent( BrowserAccessibility* node) { BrowserAccessibilityManager::FireFocusEvent(node); @@ -103,10 +152,6 @@ void BrowserAccessibilityManagerAndroid::FireBlinkEvent( if (!wcax) return; - // Sometimes we get events on nodes in our internal accessibility tree - // that aren't exposed on Android. Update |node| to point to the highest - // ancestor that's a leaf node. - node = node->PlatformGetClosestPlatformObject(); BrowserAccessibilityAndroid* android_node = static_cast<BrowserAccessibilityAndroid*>(node); @@ -133,24 +178,9 @@ void BrowserAccessibilityManagerAndroid::FireGeneratedEvent( if (!wcax) return; - // Sometimes we get events on nodes in our internal accessibility tree - // that aren't exposed on Android. Update |node| to point to the highest - // ancestor that's a leaf node. - BrowserAccessibility* original_node = node; - node = node->PlatformGetClosestPlatformObject(); BrowserAccessibilityAndroid* android_node = static_cast<BrowserAccessibilityAndroid*>(node); - // If the closest platform object is a password field, the event we're - // getting is doing something in the shadow dom, for example replacing a - // character with a dot after a short pause. On Android we don't want to - // fire an event for those changes, but we do want to make sure our internal - // state is correct, so we call OnDataChanged() and then return. - if (android_node->IsPasswordField() && original_node != node) { - android_node->OnDataChanged(); - return; - } - // Always send AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED to notify // the Android system that the accessibility hierarchy rooted at this // node has changed. @@ -390,25 +420,8 @@ void BrowserAccessibilityManagerAndroid::HandleHoverEvent( if (!wcax) return; - // First walk up to the nearest platform node, in case this node isn't - // even exposed on the platform. - node = node->PlatformGetClosestPlatformObject(); - - // If this node is uninteresting and just a wrapper around a sole - // interesting descendant, prefer that descendant instead. - const BrowserAccessibilityAndroid* android_node = + BrowserAccessibilityAndroid* android_node = static_cast<BrowserAccessibilityAndroid*>(node); - const BrowserAccessibilityAndroid* sole_interesting_node = - android_node->GetSoleInterestingNodeFromSubtree(); - if (sole_interesting_node) - android_node = sole_interesting_node; - - // Finally, if this node is still uninteresting, try to walk up to - // find an interesting parent. - while (android_node && !android_node->IsInterestingOnAndroid()) { - android_node = static_cast<BrowserAccessibilityAndroid*>( - android_node->PlatformGetParent()); - } if (android_node) wcax->HandleHover(android_node->unique_id()); diff --git a/chromium/content/browser/accessibility/browser_accessibility_manager_android.h b/chromium/content/browser/accessibility/browser_accessibility_manager_android.h index bf675e03573..2a175634095 100644 --- a/chromium/content/browser/accessibility/browser_accessibility_manager_android.h +++ b/chromium/content/browser/accessibility/browser_accessibility_manager_android.h @@ -76,6 +76,9 @@ class CONTENT_EXPORT BrowserAccessibilityManagerAndroid BrowserAccessibility* GetFocus() const override; void SendLocationChangeEvents( const std::vector<mojom::LocationChangesPtr>& changes) override; + BrowserAccessibility* RetargetForEvents( + BrowserAccessibility* node, + RetargetEventType type) const override; void FireFocusEvent(BrowserAccessibility* node) override; void FireBlinkEvent(ax::mojom::Event event_type, BrowserAccessibility* node) override; diff --git a/chromium/content/browser/accessibility/browser_accessibility_manager_mac.mm b/chromium/content/browser/accessibility/browser_accessibility_manager_mac.mm index 07645b4165b..9795a4ce389 100644 --- a/chromium/content/browser/accessibility/browser_accessibility_manager_mac.mm +++ b/chromium/content/browser/accessibility/browser_accessibility_manager_mac.mm @@ -11,7 +11,6 @@ #import "base/mac/scoped_nsobject.h" #include "base/strings/sys_string_conversions.h" #include "base/strings/utf_string_conversions.h" -#include "base/task/post_task.h" #include "base/time/time.h" #import "content/browser/accessibility/browser_accessibility_cocoa.h" #import "content/browser/accessibility/browser_accessibility_mac.h" @@ -372,8 +371,8 @@ void BrowserAccessibilityManagerMac::FireGeneratedEvent( // Use native VoiceOver support for live regions. base::scoped_nsobject<BrowserAccessibilityCocoa> retained_node( [native_node retain]); - base::PostDelayedTask( - FROM_HERE, {BrowserThread::UI}, + GetUIThreadTaskRunner({})->PostDelayedTask( + FROM_HERE, base::BindOnce( [](base::scoped_nsobject<BrowserAccessibilityCocoa> node) { if (node && [node instanceActive]) { @@ -557,10 +556,12 @@ BrowserAccessibilityManagerMac::GetUserInfoForValueChangedNotification( }]; } if (!inserted_text.empty()) { - // TODO(nektar): Figure out if this is a paste operation instead of typing. - // Changes to Blink would be required. + // TODO(nektar): Figure out if this is a paste, insertion or typing. + // Changes to Blink would be required. A heuristic is currently used. + auto edit_type = inserted_text.length() > 1 ? @(AXTextEditTypeInsert) + : @(AXTextEditTypeTyping); [changes addObject:@{ - NSAccessibilityTextEditType : @(AXTextEditTypeTyping), + NSAccessibilityTextEditType : edit_type, NSAccessibilityTextChangeValueLength : @(inserted_text.length()), NSAccessibilityTextChangeValue : base::SysUTF16ToNSString(inserted_text) }]; diff --git a/chromium/content/browser/accessibility/browser_accessibility_manager_unittest.cc b/chromium/content/browser/accessibility/browser_accessibility_manager_unittest.cc index 101b72a2ea8..971f4519665 100644 --- a/chromium/content/browser/accessibility/browser_accessibility_manager_unittest.cc +++ b/chromium/content/browser/accessibility/browser_accessibility_manager_unittest.cc @@ -126,15 +126,15 @@ TEST_F(BrowserAccessibilityManagerTest, BoundsForRange) { ui::AXNodeData static_text; static_text.id = 2; - static_text.SetName("Hello, world."); static_text.role = ax::mojom::Role::kStaticText; + static_text.SetName("Hello, world."); static_text.relative_bounds.bounds = gfx::RectF(100, 100, 29, 18); root.child_ids.push_back(2); ui::AXNodeData inline_text1; inline_text1.id = 3; - inline_text1.SetName("Hello, "); inline_text1.role = ax::mojom::Role::kInlineTextBox; + inline_text1.SetName("Hello, "); inline_text1.relative_bounds.bounds = gfx::RectF(100, 100, 29, 9); inline_text1.SetTextDirection(ax::mojom::TextDirection::kLtr); std::vector<int32_t> character_offsets1; @@ -151,8 +151,8 @@ TEST_F(BrowserAccessibilityManagerTest, BoundsForRange) { ui::AXNodeData inline_text2; inline_text2.id = 4; - inline_text2.SetName("world."); inline_text2.role = ax::mojom::Role::kInlineTextBox; + inline_text2.SetName("world."); inline_text2.relative_bounds.bounds = gfx::RectF(100, 109, 28, 9); inline_text2.SetTextDirection(ax::mojom::TextDirection::kLtr); std::vector<int32_t> character_offsets2; @@ -232,15 +232,15 @@ TEST_F(BrowserAccessibilityManagerTest, BoundsForRangeMultiElement) { ui::AXNodeData static_text; static_text.id = 2; - static_text.SetName("ABC"); static_text.role = ax::mojom::Role::kStaticText; + static_text.SetName("ABC"); static_text.relative_bounds.bounds = gfx::RectF(0, 20, 33, 9); root.child_ids.push_back(2); ui::AXNodeData inline_text1; inline_text1.id = 3; - inline_text1.SetName("ABC"); inline_text1.role = ax::mojom::Role::kInlineTextBox; + inline_text1.SetName("ABC"); inline_text1.relative_bounds.bounds = gfx::RectF(0, 20, 33, 9); inline_text1.SetTextDirection(ax::mojom::TextDirection::kLtr); std::vector<int32_t> character_offsets{10, 21, 33}; @@ -250,15 +250,15 @@ TEST_F(BrowserAccessibilityManagerTest, BoundsForRangeMultiElement) { ui::AXNodeData static_text2; static_text2.id = 4; - static_text2.SetName("ABC"); static_text2.role = ax::mojom::Role::kStaticText; + static_text2.SetName("ABC"); static_text2.relative_bounds.bounds = gfx::RectF(10, 40, 33, 9); root.child_ids.push_back(4); ui::AXNodeData inline_text2; inline_text2.id = 5; - inline_text2.SetName("ABC"); inline_text2.role = ax::mojom::Role::kInlineTextBox; + inline_text2.SetName("ABC"); inline_text2.relative_bounds.bounds = gfx::RectF(10, 40, 33, 9); inline_text2.SetTextDirection(ax::mojom::TextDirection::kLtr); inline_text2.AddIntListAttribute( @@ -350,15 +350,15 @@ TEST_F(BrowserAccessibilityManagerTest, BoundsForRangeBiDi) { ui::AXNodeData static_text; static_text.id = 2; - static_text.SetName("123abc"); static_text.role = ax::mojom::Role::kStaticText; + static_text.SetName("123abc"); static_text.relative_bounds.bounds = gfx::RectF(100, 100, 60, 20); root.child_ids.push_back(2); ui::AXNodeData inline_text1; inline_text1.id = 3; - inline_text1.SetName("123"); inline_text1.role = ax::mojom::Role::kInlineTextBox; + inline_text1.SetName("123"); inline_text1.relative_bounds.bounds = gfx::RectF(100, 100, 30, 20); inline_text1.SetTextDirection(ax::mojom::TextDirection::kLtr); std::vector<int32_t> character_offsets1; @@ -371,8 +371,8 @@ TEST_F(BrowserAccessibilityManagerTest, BoundsForRangeBiDi) { ui::AXNodeData inline_text2; inline_text2.id = 4; - inline_text2.SetName("abc"); inline_text2.role = ax::mojom::Role::kInlineTextBox; + inline_text2.SetName("abc"); inline_text2.relative_bounds.bounds = gfx::RectF(130, 100, 30, 20); inline_text2.SetTextDirection(ax::mojom::TextDirection::kRtl); std::vector<int32_t> character_offsets2; @@ -447,15 +447,15 @@ TEST_F(BrowserAccessibilityManagerTest, BoundsForRangeScrolledWindow) { ui::AXNodeData static_text; static_text.id = 2; - static_text.SetName("ABC"); static_text.role = ax::mojom::Role::kStaticText; + static_text.SetName("ABC"); static_text.relative_bounds.bounds = gfx::RectF(100, 100, 16, 9); root.child_ids.push_back(2); ui::AXNodeData inline_text; inline_text.id = 3; - inline_text.SetName("ABC"); inline_text.role = ax::mojom::Role::kInlineTextBox; + inline_text.SetName("ABC"); inline_text.relative_bounds.bounds = gfx::RectF(100, 100, 16, 9); inline_text.SetTextDirection(ax::mojom::TextDirection::kLtr); std::vector<int32_t> character_offsets1; @@ -513,28 +513,28 @@ TEST_F(BrowserAccessibilityManagerTest, BoundsForRangeOnParentElement) { ui::AXNodeData static_text1; static_text1.id = 3; - static_text1.SetName("AB"); static_text1.role = ax::mojom::Role::kStaticText; + static_text1.SetName("AB"); static_text1.relative_bounds.bounds = gfx::RectF(100, 100, 40, 20); static_text1.child_ids.push_back(6); ui::AXNodeData img; img.id = 4; - img.SetName("Test image"); img.role = ax::mojom::Role::kImage; + img.SetName("Test image"); img.relative_bounds.bounds = gfx::RectF(140, 100, 20, 20); ui::AXNodeData static_text2; static_text2.id = 5; - static_text2.SetName("CD"); static_text2.role = ax::mojom::Role::kStaticText; + static_text2.SetName("CD"); static_text2.relative_bounds.bounds = gfx::RectF(160, 100, 40, 20); static_text2.child_ids.push_back(7); ui::AXNodeData inline_text1; inline_text1.id = 6; - inline_text1.SetName("AB"); inline_text1.role = ax::mojom::Role::kInlineTextBox; + inline_text1.SetName("AB"); inline_text1.relative_bounds.bounds = gfx::RectF(100, 100, 40, 20); inline_text1.SetTextDirection(ax::mojom::TextDirection::kLtr); std::vector<int32_t> character_offsets1; @@ -545,8 +545,8 @@ TEST_F(BrowserAccessibilityManagerTest, BoundsForRangeOnParentElement) { ui::AXNodeData inline_text2; inline_text2.id = 7; - inline_text2.SetName("CD"); inline_text2.role = ax::mojom::Role::kInlineTextBox; + inline_text2.SetName("CD"); inline_text2.relative_bounds.bounds = gfx::RectF(160, 100, 40, 20); inline_text2.SetTextDirection(ax::mojom::TextDirection::kLtr); std::vector<int32_t> character_offsets2; @@ -1342,6 +1342,7 @@ TEST_F(BrowserAccessibilityManagerTest, TestHitTestScaled) { ui::AXNodeData child_child; child_child.id = 2; + child_child.role = ax::mojom::Role::kGenericContainer; child_child.SetName("child_child"); child_child.relative_bounds.bounds = gfx::RectF(0, 0, 100, 100); @@ -1358,6 +1359,7 @@ TEST_F(BrowserAccessibilityManagerTest, TestHitTestScaled) { ui::AXNodeData parent_child; parent_child.id = 2; + parent_child.role = ax::mojom::Role::kGenericContainer; parent_child.SetName("parent_child"); parent_child.relative_bounds.bounds = gfx::RectF(0, 0, 100, 100); @@ -1408,10 +1410,12 @@ TEST_F(BrowserAccessibilityManagerTest, TestShouldFireEventForNode) { ui::AXNodeData inline_text; inline_text.id = 1111; inline_text.role = ax::mojom::Role::kInlineTextBox; + inline_text.SetName("One two three."); ui::AXNodeData text; text.id = 111; text.role = ax::mojom::Role::kStaticText; + text.SetName("One two three."); text.child_ids = {inline_text.id}; ui::AXNodeData paragraph; @@ -1432,7 +1436,13 @@ TEST_F(BrowserAccessibilityManagerTest, TestShouldFireEventForNode) { EXPECT_TRUE(manager->ShouldFireEventForNode(manager->GetFromID(1))); EXPECT_TRUE(manager->ShouldFireEventForNode(manager->GetFromID(11))); EXPECT_TRUE(manager->ShouldFireEventForNode(manager->GetFromID(111))); +#if defined(OS_ANDROID) + // On Android, ShouldFireEventForNode walks up the ancestor that's a leaf node + // node and the event is fired on the updated target. + EXPECT_TRUE(manager->ShouldFireEventForNode(manager->GetFromID(1111))); +#else EXPECT_FALSE(manager->ShouldFireEventForNode(manager->GetFromID(1111))); +#endif } TEST_F(BrowserAccessibilityManagerTest, NestedChildRoot) { diff --git a/chromium/content/browser/accessibility/browser_accessibility_manager_win.cc b/chromium/content/browser/accessibility/browser_accessibility_manager_win.cc index cc3a7d1c522..579fafab388 100644 --- a/chromium/content/browser/accessibility/browser_accessibility_manager_win.cc +++ b/chromium/content/browser/accessibility/browser_accessibility_manager_win.cc @@ -23,6 +23,7 @@ #include "ui/accessibility/ax_role_properties.h" #include "ui/accessibility/platform/ax_fragment_root_win.h" #include "ui/accessibility/platform/ax_platform_node_delegate_utils_win.h" +#include "ui/accessibility/platform/uia_registrar_win.h" #include "ui/base/win/atl_module.h" namespace content { @@ -95,24 +96,11 @@ void BrowserAccessibilityManagerWin::FireBlinkEvent( if (node->GetData().IsInvocable()) FireUiaAccessibilityEvent(UIA_Invoke_InvokedEventId, node); break; - case ax::mojom::Event::kEndOfTest: { - if (::switches::IsExperimentalAccessibilityPlatformUIAEnabled()) { - // Event tests use kEndOfTest as a sentinel to mark the end of the test. - Microsoft::WRL::ComPtr<IUIAutomationRegistrar> registrar; - CoCreateInstance(CLSID_CUIAutomationRegistrar, NULL, - CLSCTX_INPROC_SERVER, IID_IUIAutomationRegistrar, - ®istrar); - CHECK(registrar.Get()); - UIAutomationEventInfo custom_event = {kUiaTestCompleteSentinelGuid, - kUiaTestCompleteSentinel}; - EVENTID custom_event_id = 0; - CHECK(SUCCEEDED( - registrar->RegisterEvent(&custom_event, &custom_event_id))); - - FireUiaAccessibilityEvent(custom_event_id, node); - } + case ax::mojom::Event::kEndOfTest: + // Event tests use kEndOfTest as a sentinel to mark the end of the test. + FireUiaAccessibilityEvent( + ui::UiaRegistrarWin::GetInstance().GetUiaTestCompleteEventId(), node); break; - } case ax::mojom::Event::kLocationChanged: FireWinAccessibilityEvent(IA2_EVENT_VISIBLE_DATA_CHANGED, node); break; @@ -171,8 +159,8 @@ void BrowserAccessibilityManagerWin::FireGeneratedEvent( aria_properties_events_.insert(node); break; case ui::AXEventGenerator::Event::CHILDREN_CHANGED: { - // If this node is ignored, notify from the platform parent if available, - // since it will be unignored. + // If this node is ignored, fire the event on the platform parent since + // ignored nodes cannot raise events. BrowserAccessibility* target_node = node->IsIgnored() ? node->PlatformGetParent() : node; if (target_node) { @@ -264,17 +252,6 @@ void BrowserAccessibilityManagerWin::FireGeneratedEvent( FireUiaAccessibilityEvent(UIA_LiveRegionChangedEventId, node); break; case ui::AXEventGenerator::Event::LIVE_REGION_CHANGED: - // This will force ATs that synchronously call get_newText (e.g., NVDA) to - // read the entire live region hypertext. - ToBrowserAccessibilityWin(node)->GetCOM()->ForceNewHypertext(); - // TODO(accessibility) Technically this should only be fired if the new - // text is non-empty. Also, IA2_EVENT_TEXT_REMOVED should be fired if - // there was non-empty old text. However, this does not known to affect - // any current screen reader behavior either way. It could affect - // the aria-relevant="removals" case, but that in general is poorly - // supported markup across browser-AT combinations, and not recommended. - FireWinAccessibilityEvent(IA2_EVENT_TEXT_INSERTED, node); - // This event is redundant with the IA2_EVENT_TEXT_INSERTED events; // however, JAWS 2018 and earlier do not process the text inserted // events when "virtual cursor mode" is turned off (Insert+Z). @@ -310,11 +287,9 @@ void BrowserAccessibilityManagerWin::FireGeneratedEvent( break; case ui::AXEventGenerator::Event::NAME_CHANGED: FireUiaPropertyChangedEvent(UIA_NamePropertyId, node); - // Only fire name changes when the name comes from an attribute, and is - // not contained within an active live-region; otherwise name changes are - // redundant with text removed/inserted events. - if (node->GetData().GetNameFrom() != ax::mojom::NameFrom::kContents && - !node->GetData().IsContainedInActiveLiveRegion()) + // Only fire name changes when the name comes from an attribute, otherwise + // name changes are redundant with text removed/inserted events. + if (node->GetData().GetNameFrom() != ax::mojom::NameFrom::kContents) FireWinAccessibilityEvent(EVENT_OBJECT_NAMECHANGE, node); break; case ui::AXEventGenerator::Event::PLACEHOLDER_CHANGED: @@ -424,10 +399,13 @@ void BrowserAccessibilityManagerWin::FireWinAccessibilityEvent( // Suppress events when |IGNORED_CHANGED| except for related SHOW / HIDE. // Also include MENUPOPUPSTART / MENUPOPUPEND since a change in the ignored // state may show / hide a popup by exposing it to the tree or not. + // Also include focus events since a node may become visible at the same time + // it receives focus It's never good to suppress a po if (base::Contains(ignored_changed_nodes_, node)) { switch (win_event_type) { case EVENT_OBJECT_HIDE: case EVENT_OBJECT_SHOW: + case EVENT_OBJECT_FOCUS: case EVENT_SYSTEM_MENUPOPUPEND: case EVENT_SYSTEM_MENUPOPUPSTART: break; @@ -499,7 +477,7 @@ void BrowserAccessibilityManagerWin::FireUiaPropertyChangedEvent( auto* provider = ToBrowserAccessibilityWin(node)->GetCOM(); base::win::ScopedVariant new_value; if (SUCCEEDED( - provider->GetPropertyValue(uia_property, new_value.Receive()))) { + provider->GetPropertyValueImpl(uia_property, new_value.Receive()))) { ::UiaRaiseAutomationPropertyChangedEvent(provider, uia_property, old_value, new_value); } diff --git a/chromium/content/browser/accessibility/browser_accessibility_manager_win.h b/chromium/content/browser/accessibility/browser_accessibility_manager_win.h index d1baa803849..20c72281451 100644 --- a/chromium/content/browser/accessibility/browser_accessibility_manager_win.h +++ b/chromium/content/browser/accessibility/browser_accessibility_manager_win.h @@ -19,14 +19,6 @@ namespace content { class BrowserAccessibilityWin; -// {3761326A-34B2-465A-835D-7A3D8F4EFB92} -static const GUID kUiaTestCompleteSentinelGuid = { - 0x3761326a, - 0x34b2, - 0x465a, - {0x83, 0x5d, 0x7a, 0x3d, 0x8f, 0x4e, 0xfb, 0x92}}; -static const wchar_t kUiaTestCompleteSentinel[] = L"kUiaTestCompleteSentinel"; - // Manages a tree of BrowserAccessibilityWin objects. class CONTENT_EXPORT BrowserAccessibilityManagerWin : public BrowserAccessibilityManager { diff --git a/chromium/content/browser/accessibility/browser_accessibility_state_impl.cc b/chromium/content/browser/accessibility/browser_accessibility_state_impl.cc index 352063f1006..68243d5ec48 100644 --- a/chromium/content/browser/accessibility/browser_accessibility_state_impl.cc +++ b/chromium/content/browser/accessibility/browser_accessibility_state_impl.cc @@ -11,7 +11,6 @@ #include "base/debug/crash_logging.h" #include "base/metrics/histogram_functions.h" #include "base/metrics/histogram_macros.h" -#include "base/task/post_task.h" #include "base/task/thread_pool.h" #include "build/build_config.h" #include "content/browser/renderer_host/render_widget_host_impl.h" @@ -89,8 +88,8 @@ BrowserAccessibilityStateImpl::BrowserAccessibilityStateImpl() base::TimeDelta::FromSeconds(ACCESSIBILITY_HISTOGRAM_DELAY_SECS)); // Other things must be done on the UI thread (e.g. to access PrefService). - base::PostDelayedTask( - FROM_HERE, {BrowserThread::UI}, + GetUIThreadTaskRunner({})->PostDelayedTask( + FROM_HERE, base::BindOnce(&BrowserAccessibilityStateImpl::UpdateHistogramsOnUIThread, this), base::TimeDelta::FromSeconds(ACCESSIBILITY_HISTOGRAM_DELAY_SECS)); @@ -178,8 +177,9 @@ void BrowserAccessibilityStateImpl::UpdateHistogramsOnUIThread() { #if defined(OS_WIN) UMA_HISTOGRAM_ENUMERATION( "Accessibility.WinHighContrastTheme", - ui::NativeTheme::GetInstanceForNativeUi()->GetHighContrastColorScheme(), - ui::NativeTheme::HighContrastColorScheme::kMaxValue); + ui::NativeTheme::GetInstanceForNativeUi() + ->GetPlatformHighContrastColorScheme(), + ui::NativeTheme::PlatformHighContrastColorScheme::kMaxValue); #endif } diff --git a/chromium/content/browser/accessibility/browser_accessibility_state_impl_mac.mm b/chromium/content/browser/accessibility/browser_accessibility_state_impl_mac.mm index 7297514e88a..7a59582ea63 100644 --- a/chromium/content/browser/accessibility/browser_accessibility_state_impl_mac.mm +++ b/chromium/content/browser/accessibility/browser_accessibility_state_impl_mac.mm @@ -8,6 +8,7 @@ #include "base/metrics/histogram_macros.h" #include "content/browser/web_contents/web_contents_impl.h" +#include "content/public/browser/browser_thread.h" #include "ui/gfx/animation/animation.h" @interface NSWorkspace (Partials) @@ -52,8 +53,8 @@ void SetupAccessibilityDisplayOptionsNotifier() { } // namespace void BrowserAccessibilityStateImpl::PlatformInitialize() { - base::PostTask(FROM_HERE, {BrowserThread::UI}, - base::BindOnce(&SetupAccessibilityDisplayOptionsNotifier)); + GetUIThreadTaskRunner({})->PostTask( + FROM_HERE, base::BindOnce(&SetupAccessibilityDisplayOptionsNotifier)); } void BrowserAccessibilityStateImpl:: diff --git a/chromium/content/browser/accessibility/browser_accessibility_unittest.cc b/chromium/content/browser/accessibility/browser_accessibility_unittest.cc index e0d1eadd2b0..ed559c391b0 100644 --- a/chromium/content/browser/accessibility/browser_accessibility_unittest.cc +++ b/chromium/content/browser/accessibility/browser_accessibility_unittest.cc @@ -73,7 +73,13 @@ TEST_F(BrowserAccessibilityTest, TestCanFireEvents) { BrowserAccessibility* text_obj = manager->GetFromID(111); EXPECT_TRUE(text_obj->PlatformIsLeaf()); +#if !defined(OS_ANDROID) EXPECT_TRUE(text_obj->CanFireEvents()); +#endif + BrowserAccessibility* retarget = manager->RetargetForEvents( + text_obj, BrowserAccessibilityManager::RetargetEventType:: + RetargetEventTypeBlinkHover); + EXPECT_TRUE(retarget->CanFireEvents()); manager.reset(); } @@ -273,15 +279,15 @@ TEST_F(BrowserAccessibilityTest, GetInnerTextRangeBoundsRect) { ui::AXNodeData static_text; static_text.id = 2; - static_text.SetName("Hello, world."); static_text.role = ax::mojom::Role::kStaticText; + static_text.SetName("Hello, world."); static_text.relative_bounds.bounds = gfx::RectF(100, 100, 29, 18); root.child_ids.push_back(2); ui::AXNodeData inline_text1; inline_text1.id = 3; - inline_text1.SetName("Hello, "); inline_text1.role = ax::mojom::Role::kInlineTextBox; + inline_text1.SetName("Hello, "); inline_text1.relative_bounds.bounds = gfx::RectF(100, 100, 29, 9); inline_text1.SetTextDirection(ax::mojom::TextDirection::kLtr); std::vector<int32_t> character_offsets1; @@ -298,8 +304,8 @@ TEST_F(BrowserAccessibilityTest, GetInnerTextRangeBoundsRect) { ui::AXNodeData inline_text2; inline_text2.id = 4; - inline_text2.SetName("world."); inline_text2.role = ax::mojom::Role::kInlineTextBox; + inline_text2.SetName("world."); inline_text2.relative_bounds.bounds = gfx::RectF(100, 109, 28, 9); inline_text2.SetTextDirection(ax::mojom::TextDirection::kLtr); std::vector<int32_t> character_offsets2; @@ -394,15 +400,15 @@ TEST_F(BrowserAccessibilityTest, GetInnerTextRangeBoundsRectMultiElement) { ui::AXNodeData static_text; static_text.id = 2; - static_text.SetName("ABC"); static_text.role = ax::mojom::Role::kStaticText; + static_text.SetName("ABC"); static_text.relative_bounds.bounds = gfx::RectF(0, 20, 33, 9); root.child_ids.push_back(2); ui::AXNodeData inline_text1; inline_text1.id = 3; - inline_text1.SetName("ABC"); inline_text1.role = ax::mojom::Role::kInlineTextBox; + inline_text1.SetName("ABC"); inline_text1.relative_bounds.bounds = gfx::RectF(0, 20, 33, 9); inline_text1.SetTextDirection(ax::mojom::TextDirection::kLtr); std::vector<int32_t> character_offsets{10, 21, 33}; @@ -412,15 +418,15 @@ TEST_F(BrowserAccessibilityTest, GetInnerTextRangeBoundsRectMultiElement) { ui::AXNodeData static_text2; static_text2.id = 4; - static_text2.SetName("ABC"); static_text2.role = ax::mojom::Role::kStaticText; + static_text2.SetName("ABC"); static_text2.relative_bounds.bounds = gfx::RectF(10, 40, 33, 9); root.child_ids.push_back(4); ui::AXNodeData inline_text2; inline_text2.id = 5; - inline_text2.SetName("ABC"); inline_text2.role = ax::mojom::Role::kInlineTextBox; + inline_text2.SetName("ABC"); inline_text2.relative_bounds.bounds = gfx::RectF(10, 40, 33, 9); inline_text2.SetTextDirection(ax::mojom::TextDirection::kLtr); inline_text2.AddIntListAttribute( @@ -521,15 +527,15 @@ TEST_F(BrowserAccessibilityTest, GetInnerTextRangeBoundsRectBiDi) { ui::AXNodeData static_text; static_text.id = 2; - static_text.SetName("123abc"); static_text.role = ax::mojom::Role::kStaticText; + static_text.SetName("123abc"); static_text.relative_bounds.bounds = gfx::RectF(100, 100, 60, 20); root.child_ids.push_back(2); ui::AXNodeData inline_text1; inline_text1.id = 3; - inline_text1.SetName("123"); inline_text1.role = ax::mojom::Role::kInlineTextBox; + inline_text1.SetName("123"); inline_text1.relative_bounds.bounds = gfx::RectF(100, 100, 30, 20); inline_text1.SetTextDirection(ax::mojom::TextDirection::kLtr); std::vector<int32_t> character_offsets1; @@ -542,8 +548,8 @@ TEST_F(BrowserAccessibilityTest, GetInnerTextRangeBoundsRectBiDi) { ui::AXNodeData inline_text2; inline_text2.id = 4; - inline_text2.SetName("abc"); inline_text2.role = ax::mojom::Role::kInlineTextBox; + inline_text2.SetName("abc"); inline_text2.relative_bounds.bounds = gfx::RectF(130, 100, 30, 20); inline_text2.SetTextDirection(ax::mojom::TextDirection::kRtl); std::vector<int32_t> character_offsets2; @@ -621,15 +627,15 @@ TEST_F(BrowserAccessibilityTest, GetInnerTextRangeBoundsRectScrolledWindow) { ui::AXNodeData static_text; static_text.id = 2; - static_text.SetName("ABC"); static_text.role = ax::mojom::Role::kStaticText; + static_text.SetName("ABC"); static_text.relative_bounds.bounds = gfx::RectF(100, 100, 16, 9); root.child_ids.push_back(2); ui::AXNodeData inline_text; inline_text.id = 3; - inline_text.SetName("ABC"); inline_text.role = ax::mojom::Role::kInlineTextBox; + inline_text.SetName("ABC"); inline_text.relative_bounds.bounds = gfx::RectF(100, 100, 16, 9); inline_text.SetTextDirection(ax::mojom::TextDirection::kLtr); std::vector<int32_t> character_offsets1; @@ -702,14 +708,14 @@ TEST_F(BrowserAccessibilityTest, NextWordPositionWithHypertext) { ui::AXNodeData input; input.id = 2; input.role = ax::mojom::Role::kTextField; - input.child_ids = {3}; input.SetName("Search the web"); + input.child_ids = {3}; ui::AXNodeData static_text; static_text.id = 3; static_text.role = ax::mojom::Role::kStaticText; - static_text.child_ids = {4}; static_text.SetName("Search the web"); + static_text.child_ids = {4}; ui::AXNodeData inline_text; inline_text.id = 4; @@ -830,8 +836,8 @@ TEST_F(BrowserAccessibilityTest, GetIndexInParent) { ui::AXNodeData static_text; static_text.id = 2; - static_text.SetName("ABC"); static_text.role = ax::mojom::Role::kStaticText; + static_text.SetName("ABC"); std::unique_ptr<BrowserAccessibilityManager> browser_accessibility_manager( BrowserAccessibilityManager::Create( diff --git a/chromium/content/browser/accessibility/browser_accessibility_win.cc b/chromium/content/browser/accessibility/browser_accessibility_win.cc index e92df444a70..780a3f52232 100644 --- a/chromium/content/browser/accessibility/browser_accessibility_win.cc +++ b/chromium/content/browser/accessibility/browser_accessibility_win.cc @@ -3,6 +3,7 @@ // found in the LICENSE file. #include "content/browser/accessibility/browser_accessibility_win.h" + #include "content/browser/accessibility/browser_accessibility_manager.h" #include "content/browser/accessibility/browser_accessibility_state_impl.h" @@ -40,18 +41,6 @@ void BrowserAccessibilityWin::UpdatePlatformAttributes() { GetCOM()->UpdateStep3FireEvents(); } -bool BrowserAccessibilityWin::PlatformIsLeafIncludingIgnored() const { - // On Windows, we want to hide the subtree of a collapsed <select> element. - // Otherwise, ATs are always going to announce its options whether it's - // collapsed or expanded. In the AXTree, this element corresponds to a node - // with role ax::mojom::Role::kPopUpButton parent of a node with role - // ax::mojom::Role::kMenuListPopup. - if (IsCollapsedMenuListPopUpButton()) - return true; - - return BrowserAccessibility::PlatformIsLeafIncludingIgnored(); -} - bool BrowserAccessibilityWin::CanFireEvents() const { // On Windows, we want to hide the subtree of a collapsed <select> element but // we still need to fire events on those hidden nodes. diff --git a/chromium/content/browser/accessibility/browser_accessibility_win.h b/chromium/content/browser/accessibility/browser_accessibility_win.h index 18bcad491c6..f7dcfaba976 100644 --- a/chromium/content/browser/accessibility/browser_accessibility_win.h +++ b/chromium/content/browser/accessibility/browser_accessibility_win.h @@ -5,6 +5,8 @@ #ifndef CONTENT_BROWSER_ACCESSIBILITY_BROWSER_ACCESSIBILITY_WIN_H_ #define CONTENT_BROWSER_ACCESSIBILITY_BROWSER_ACCESSIBILITY_WIN_H_ +#include <vector> + #include "base/win/atl.h" #include "content/browser/accessibility/browser_accessibility.h" #include "content/browser/accessibility/browser_accessibility_com_win.h" @@ -23,9 +25,9 @@ class CONTENT_EXPORT BrowserAccessibilityWin : public BrowserAccessibility { void UpdatePlatformAttributes() override; // - // BrowserAccessibility methods. + // BrowserAccessibility overrides. // - bool PlatformIsLeafIncludingIgnored() const override; + bool CanFireEvents() const override; ui::AXPlatformNode* GetAXPlatformNode() const override; void OnLocationChanged() override; diff --git a/chromium/content/browser/accessibility/browser_accessibility_win_unittest.cc b/chromium/content/browser/accessibility/browser_accessibility_win_unittest.cc index 69cac2a162c..ca9831ee1e2 100644 --- a/chromium/content/browser/accessibility/browser_accessibility_win_unittest.cc +++ b/chromium/content/browser/accessibility/browser_accessibility_win_unittest.cc @@ -4,6 +4,9 @@ #include "content/browser/accessibility/browser_accessibility_win.h" +#include <string> +#include <vector> + #include <objbase.h> #include <stdint.h> #include <wrl/client.h> @@ -129,18 +132,18 @@ TEST_F(BrowserAccessibilityWinTest, TestNoLeaks) { // BrowserAccessibilityManager. ui::AXNodeData button; button.id = 2; - button.SetName("Button"); button.role = ax::mojom::Role::kButton; + button.SetName("Button"); ui::AXNodeData checkbox; checkbox.id = 3; - checkbox.SetName("Checkbox"); checkbox.role = ax::mojom::Role::kCheckBox; + checkbox.SetName("Checkbox"); ui::AXNodeData root; root.id = 1; - root.SetName("Document"); root.role = ax::mojom::Role::kRootWebArea; + root.SetName("Document"); root.child_ids.push_back(2); root.child_ids.push_back(3); @@ -193,8 +196,8 @@ TEST_F(BrowserAccessibilityWinTest, TestChildrenChange) { ui::AXNodeData root; root.id = 1; - root.SetName("Document"); root.role = ax::mojom::Role::kRootWebArea; + root.SetName("Document"); root.child_ids.push_back(2); // Construct a BrowserAccessibilityManager with this @@ -701,22 +704,28 @@ TEST_F(BrowserAccessibilityWinTest, TestComplexHypertext) { 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)); ui::AXNodeData button, button_text; button.id = 15; button_text.id = 17; - button_text.SetName(base::UTF16ToUTF8(button_text_name)); 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); ui::AXNodeData link, link_text; link.id = 16; link_text.id = 18; - link_text.SetName(base::UTF16ToUTF8(link_text_name)); 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); ui::AXNodeData root; @@ -1068,15 +1077,15 @@ TEST_F(BrowserAccessibilityWinTest, TestIA2Attributes) { ui::AXNodeData checkbox; checkbox.id = 3; - checkbox.SetName("Checkbox"); checkbox.role = ax::mojom::Role::kCheckBox; checkbox.SetCheckedState(ax::mojom::CheckedState::kTrue); + checkbox.SetName("Checkbox"); ui::AXNodeData root; root.id = 1; - root.SetName("Document"); 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); @@ -1112,7 +1121,7 @@ TEST_F(BrowserAccessibilityWinTest, TestIA2Attributes) { EXPECT_EQ(S_OK, hr); EXPECT_NE(nullptr, attributes.Get()); attributes_str = std::wstring(attributes.Get(), attributes.Length()); - EXPECT_EQ(L"checkable:true;", attributes_str); + EXPECT_EQ(L"checkable:true;explicit-name:true;", attributes_str); manager.reset(); } @@ -1126,10 +1135,10 @@ TEST_F(BrowserAccessibilityWinTest, TestValueAttributeInTextControls) { ui::AXNodeData combo_box, combo_box_text; combo_box.id = 2; combo_box_text.id = 3; - combo_box.SetName("Combo box:"); - combo_box_text.SetName("Combo box text"); combo_box.role = ax::mojom::Role::kTextFieldWithComboBox; combo_box_text.role = ax::mojom::Role::kStaticText; + combo_box.SetName("Combo box:"); + combo_box_text.SetName("Combo box text"); combo_box.AddBoolAttribute(ax::mojom::BoolAttribute::kEditableRoot, true); combo_box.AddState(ax::mojom::State::kEditable); combo_box.AddState(ax::mojom::State::kRichlyEditable); @@ -1142,12 +1151,12 @@ TEST_F(BrowserAccessibilityWinTest, TestValueAttributeInTextControls) { search_box.id = 4; search_box_text.id = 5; new_line.id = 6; - search_box.SetName("Search for:"); - search_box_text.SetName("Search box text"); - new_line.SetName("\n"); search_box.role = ax::mojom::Role::kSearchBox; search_box_text.role = ax::mojom::Role::kStaticText; new_line.role = ax::mojom::Role::kLineBreak; + search_box.SetName("Search for:"); + search_box_text.SetName("Search box text"); + new_line.SetName("\n"); search_box.AddBoolAttribute(ax::mojom::BoolAttribute::kEditableRoot, true); search_box.AddState(ax::mojom::State::kEditable); search_box.AddState(ax::mojom::State::kRichlyEditable); @@ -1170,18 +1179,18 @@ TEST_F(BrowserAccessibilityWinTest, TestValueAttributeInTextControls) { ui::AXNodeData link, link_text; link.id = 8; link_text.id = 9; - link_text.SetName("Link text"); 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); ui::AXNodeData slider, slider_text; slider.id = 10; slider_text.id = 11; - slider.AddFloatAttribute(ax::mojom::FloatAttribute::kValueForRange, 5.0F); - slider_text.SetName("Slider text"); 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.push_back(2); // Combo box. @@ -2227,11 +2236,11 @@ TEST_F(BrowserAccessibilityWinTest, TestIAccessibleHyperlink) { 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(2); - div.child_ids.push_back(3); - div.child_ids.push_back(4); + root.child_ids.push_back(div.id); + div.child_ids = {text.id, link.id}; std::unique_ptr<BrowserAccessibilityManager> manager( BrowserAccessibilityManager::Create( diff --git a/chromium/content/browser/accessibility/dump_accessibility_browsertest_base.cc b/chromium/content/browser/accessibility/dump_accessibility_browsertest_base.cc index e90ddb72ebd..cd07323d855 100644 --- a/chromium/content/browser/accessibility/dump_accessibility_browsertest_base.cc +++ b/chromium/content/browser/accessibility/dump_accessibility_browsertest_base.cc @@ -157,6 +157,7 @@ DumpAccessibilityTestBase::DumpUnfilteredAccessibilityTreeAsString() { void DumpAccessibilityTestBase::ParseHtmlForExtraDirectives( const std::string& test_html, + std::vector<std::string>* no_load_expected, std::vector<std::string>* wait_for, std::vector<std::string>* execute, std::vector<std::string>* run_until, @@ -167,6 +168,7 @@ void DumpAccessibilityTestBase::ParseHtmlForExtraDirectives( const std::string& allow_str = formatter_->GetAllowString(); const std::string& deny_str = formatter_->GetDenyString(); const std::string& deny_node_str = formatter_->GetDenyNodeString(); + const std::string& no_load_expected_str = "@NO-LOAD-EXPECTED:"; const std::string& wait_str = "@WAIT-FOR:"; const std::string& execute_str = "@EXECUTE-AND-WAIT-FOR:"; const std::string& until_str = "@RUN-UNTIL-EVENT:"; @@ -194,6 +196,9 @@ void DumpAccessibilityTestBase::ParseHtmlForExtraDirectives( node_filters_.push_back( NodeFilter(parts[0], base::UTF8ToUTF16(parts[1]))); } + } else if (base::StartsWith(line, no_load_expected_str, + base::CompareCase::SENSITIVE)) { + no_load_expected->push_back(line.substr(no_load_expected_str.size())); } else if (base::StartsWith(line, wait_str, base::CompareCase::SENSITIVE)) { wait_for->push_back(line.substr(wait_str.size())); } else if (base::StartsWith(line, execute_str, @@ -284,6 +289,7 @@ void DumpAccessibilityTestBase::RunTestForPlatform( } // Parse filters and other directives in the test file. + std::vector<std::string> no_load_expected; std::vector<std::string> wait_for; std::vector<std::string> execute; std::vector<std::string> run_until; @@ -292,8 +298,8 @@ void DumpAccessibilityTestBase::RunTestForPlatform( node_filters_.clear(); formatter_->AddDefaultFilters(&property_filters_); AddDefaultFilters(&property_filters_); - ParseHtmlForExtraDirectives(html_contents, &wait_for, &execute, &run_until, - &default_action_on); + ParseHtmlForExtraDirectives(html_contents, &no_load_expected, &wait_for, + &execute, &run_until, &default_action_on); // Get the test URL. GURL url(embedded_test_server()->GetURL("/" + std::string(file_dir) + "/" + @@ -343,7 +349,7 @@ void DumpAccessibilityTestBase::RunTestForPlatform( waiter.WaitForNotification(); } - WaitForAXTreeLoaded(web_contents, wait_for); + WaitForAXTreeLoaded(web_contents, no_load_expected, wait_for); // Call the subclass to dump the output. std::vector<std::string> actual_lines = Dump(run_until); @@ -387,6 +393,7 @@ void DumpAccessibilityTestBase::RunTestForPlatform( void DumpAccessibilityTestBase::WaitForAXTreeLoaded( WebContentsImpl* web_contents, + const std::vector<std::string>& no_load_expected, const std::vector<std::string>& wait_for) { // Get the url of every frame in the frame tree. FrameTree* frame_tree = web_contents->GetFrameTree(); @@ -401,8 +408,18 @@ void DumpAccessibilityTestBase::WaitForAXTreeLoaded( // // We also ignore frame tree nodes created for portals in the outer // WebContents as the node doesn't have a url set. + std::string url = node->current_url().spec(); - if (url != url::kAboutBlankURL && !url.empty() && + + // sometimes we expect a url to never load, in these cases, don't wait. + bool skip_url = false; + for (std::string no_load_url : no_load_expected) { + if (url.find(no_load_url) != std::string::npos) { + skip_url = true; + break; + } + } + if (!skip_url && url != url::kAboutBlankURL && !url.empty() && node->frame_owner_element_type() != blink::mojom::FrameOwnerElementType::kPortal) { all_frame_urls.push_back(url); @@ -466,7 +483,7 @@ void DumpAccessibilityTestBase::WaitForAXTreeLoaded( for (WebContents* inner_contents : web_contents->GetInnerWebContents()) { WaitForAXTreeLoaded(static_cast<WebContentsImpl*>(inner_contents), - std::vector<std::string>()); + no_load_expected, std::vector<std::string>()); } } diff --git a/chromium/content/browser/accessibility/dump_accessibility_browsertest_base.h b/chromium/content/browser/accessibility/dump_accessibility_browsertest_base.h index 259665069eb..e815a4bf97f 100644 --- a/chromium/content/browser/accessibility/dump_accessibility_browsertest_base.h +++ b/chromium/content/browser/accessibility/dump_accessibility_browsertest_base.h @@ -97,6 +97,7 @@ class DumpAccessibilityTestBase : public ContentBrowserTest, // string to appear before comparing the results. There can be multiple // @WAIT-FOR: directives. void ParseHtmlForExtraDirectives(const std::string& test_html, + std::vector<std::string>* no_load_expected, std::vector<std::string>* wait_for, std::vector<std::string>* execute, std::vector<std::string>* run_until, @@ -139,6 +140,7 @@ class DumpAccessibilityTestBase : public ContentBrowserTest, const std::string& name); void WaitForAXTreeLoaded(WebContentsImpl* web_contents, + const std::vector<std::string>& no_load_expected, const std::vector<std::string>& wait_for); }; diff --git a/chromium/content/browser/accessibility/dump_accessibility_events_browsertest.cc b/chromium/content/browser/accessibility/dump_accessibility_events_browsertest.cc index bdbeed607fa..25a997f2935 100644 --- a/chromium/content/browser/accessibility/dump_accessibility_events_browsertest.cc +++ b/chromium/content/browser/accessibility/dump_accessibility_events_browsertest.cc @@ -774,8 +774,9 @@ IN_PROC_BROWSER_TEST_P(DumpAccessibilityEventsTest, RunEventTest(FILE_PATH_LITERAL("tabindex-removed-on-plain-div.html")); } -IN_PROC_BROWSER_TEST_P(DumpAccessibilityEventsTest, - AccessibilityEventsTabindexRemovedOnAriaHidden) { +IN_PROC_BROWSER_TEST_P( + DumpAccessibilityEventsTest, + DISABLED_AccessibilityEventsTabindexRemovedOnAriaHidden) { RunEventTest(FILE_PATH_LITERAL("tabindex-removed-on-aria-hidden.html")); } diff --git a/chromium/content/browser/accessibility/dump_accessibility_tree_browsertest.cc b/chromium/content/browser/accessibility/dump_accessibility_tree_browsertest.cc index c4bc659e60c..86b7113203c 100644 --- a/chromium/content/browser/accessibility/dump_accessibility_tree_browsertest.cc +++ b/chromium/content/browser/accessibility/dump_accessibility_tree_browsertest.cc @@ -27,6 +27,7 @@ #include "content/public/test/browser_test.h" #include "content/public/test/content_browser_test_utils.h" #include "content/shell/browser/shell.h" +#include "ui/accessibility/accessibility_features.h" #include "ui/accessibility/accessibility_switches.h" #if defined(OS_MACOSX) @@ -211,6 +212,8 @@ void DumpAccessibilityTreeTest::AddDefaultFilters( AddPropertyFilter(property_filters, "layout-guess:*", PropertyFilter::ALLOW); AddPropertyFilter(property_filters, "select*"); + AddPropertyFilter(property_filters, "selectedFromFocus=*", + PropertyFilter::DENY); AddPropertyFilter(property_filters, "descript*"); AddPropertyFilter(property_filters, "check*"); AddPropertyFilter(property_filters, "horizontal"); @@ -867,6 +870,26 @@ IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest, AccessibilityAriaModal) { RunAriaTest(FILE_PATH_LITERAL("aria-modal.html")); } +IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest, + AccessibilityAriaModalFocusableDialog) { + RunAriaTest(FILE_PATH_LITERAL("aria-modal-focusable-dialog.html")); +} + +IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest, + AccessibilityAriaModalLayered) { + RunAriaTest(FILE_PATH_LITERAL("aria-modal-layered.html")); +} + +IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest, + AccessibilityAriaModalMoveFocus) { + RunAriaTest(FILE_PATH_LITERAL("aria-modal-move-focus.html")); +} + +IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest, + AccessibilityAriaModalRemoveParentContainer) { + RunAriaTest(FILE_PATH_LITERAL("aria-modal-remove-parent-container.html")); +} + IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest, AccessibilityAriaMultiline) { RunAriaTest(FILE_PATH_LITERAL("aria-multiline.html")); } @@ -2125,6 +2148,22 @@ IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest, AccessibilitySelect) { RunHtmlTest(FILE_PATH_LITERAL("select.html")); } +IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest, + AccessibilitySelectFollowsFocus) { + RunHtmlTest(FILE_PATH_LITERAL("select-follows-focus.html")); +} + +IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest, + AccessibilitySelectFollowsFocusAriaSelectedFalse) { + RunHtmlTest( + FILE_PATH_LITERAL("select-follows-focus-aria-selected-false.html")); +} + +IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest, + AccessibilitySelectFollowsFocusMultiselect) { + RunHtmlTest(FILE_PATH_LITERAL("select-follows-focus-multiselect.html")); +} + #if defined(OS_LINUX) #define MAYBE_AccessibilitySource DISABLED_AccessibilitySource #else diff --git a/chromium/content/browser/accessibility/fullscreen_browsertest.cc b/chromium/content/browser/accessibility/fullscreen_browsertest.cc index b32920ef5b5..acb1afe3ea7 100644 --- a/chromium/content/browser/accessibility/fullscreen_browsertest.cc +++ b/chromium/content/browser/accessibility/fullscreen_browsertest.cc @@ -52,8 +52,7 @@ class FakeFullscreenDelegate : public WebContentsDelegate { ~FakeFullscreenDelegate() override = default; void EnterFullscreenModeForTab( - WebContents*, - const GURL&, + RenderFrameHost*, const blink::mojom::FullscreenOptions&) override { is_fullscreen_ = true; } diff --git a/chromium/content/browser/accessibility/hit_testing_browsertest.cc b/chromium/content/browser/accessibility/hit_testing_browsertest.cc index 7c4afb60284..26cbd0452b3 100644 --- a/chromium/content/browser/accessibility/hit_testing_browsertest.cc +++ b/chromium/content/browser/accessibility/hit_testing_browsertest.cc @@ -5,6 +5,7 @@ #include "content/browser/accessibility/hit_testing_browsertest.h" #include "base/check.h" +#include "base/test/bind_test_util.h" #include "build/build_config.h" #include "build/chromecast_buildflags.h" #include "content/browser/accessibility/accessibility_tree_formatter_blink.h" @@ -138,7 +139,8 @@ AccessibilityHitTestingBrowserTest::HitTestAndWaitForResultWithEvent( action_data.action = ax::mojom::Action::kHitTest; action_data.target_point = CSSToFramePoint(point); action_data.hit_test_event_to_fire = event_to_fire; - manager->delegate()->AccessibilityPerformAction(action_data); + manager->delegate()->AccessibilityHitTest(CSSToFramePoint(point), + event_to_fire, 0, {}); event_waiter.WaitForNotification(); RenderFrameHostImpl* target_frame = event_waiter.event_render_frame_host(); @@ -156,6 +158,30 @@ AccessibilityHitTestingBrowserTest::HitTestAndWaitForResult( } BrowserAccessibility* +AccessibilityHitTestingBrowserTest::AsyncHitTestAndWaitForCallback( + const gfx::Point& point) { + BrowserAccessibilityManager* manager = GetRootBrowserAccessibilityManager(); + + gfx::Point target_point = CSSToFramePoint(point); + base::RunLoop run_loop; + BrowserAccessibilityManager* hit_manager = nullptr; + int hit_node_id = 0; + + auto callback = [&](BrowserAccessibilityManager* manager, int node_id) { + hit_manager = manager; + hit_node_id = node_id; + run_loop.QuitClosure().Run(); + }; + manager->delegate()->AccessibilityHitTest( + target_point, ax::mojom::Event::kNone, 0, + base::BindLambdaForTesting(callback)); + run_loop.Run(); + + BrowserAccessibility* hit_node = hit_manager->GetFromID(hit_node_id); + return hit_node; +} + +BrowserAccessibility* AccessibilityHitTestingBrowserTest::CallCachingAsyncHitTest( const gfx::Point& page_point) { gfx::Point screen_point = CSSToPhysicalPixelPoint(page_point); @@ -362,6 +388,10 @@ IN_PROC_BROWSER_TEST_P(AccessibilityHitTestingBrowserTest, HitTest) { BrowserAccessibility* expected_node = FindNode(ax::mojom::Role::kGenericContainer, "rect2"); EXPECT_ACCESSIBILITY_HIT_TEST_RESULT(rect_2_point, expected_node, hit_node); + + // Try callback API. + hit_node = AsyncHitTestAndWaitForCallback(rect_2_point); + EXPECT_ACCESSIBILITY_HIT_TEST_RESULT(rect_2_point, expected_node, hit_node); } // Test a hit on a rect in the iframe. @@ -372,6 +402,10 @@ IN_PROC_BROWSER_TEST_P(AccessibilityHitTestingBrowserTest, HitTest) { FindNode(ax::mojom::Role::kGenericContainer, "rectB"); EXPECT_ACCESSIBILITY_HIT_TEST_RESULT(rect_b_point, expected_node, hit_node); + // Try callback API. + hit_node = AsyncHitTestAndWaitForCallback(rect_b_point); + EXPECT_ACCESSIBILITY_HIT_TEST_RESULT(rect_b_point, expected_node, hit_node); + // Test with a different event. hit_node = HitTestAndWaitForResultWithEvent(rect_b_point, ax::mojom::Event::kAlert); @@ -400,7 +434,14 @@ IN_PROC_BROWSER_TEST_P(AccessibilityHitTestingBrowserTest, EXPECT_TRUE(NavigateToURL(shell(), url)); waiter.WaitForNotification(); - BrowserAccessibility* hit_node = HitTestAndWaitForResult(gfx::Point(-1, -1)); + gfx::Point out_of_bounds_point(-1, -1); + + BrowserAccessibility* hit_node = HitTestAndWaitForResult(out_of_bounds_point); + ASSERT_TRUE(hit_node != nullptr); + ASSERT_EQ(ax::mojom::Role::kRootWebArea, hit_node->GetRole()); + + // Try callback API. + hit_node = AsyncHitTestAndWaitForCallback(out_of_bounds_point); ASSERT_TRUE(hit_node != nullptr); ASSERT_EQ(ax::mojom::Role::kRootWebArea, hit_node->GetRole()); } @@ -448,6 +489,10 @@ IN_PROC_BROWSER_TEST_P(AccessibilityHitTestingCrossProcessBrowserTest, BrowserAccessibility* expected_node = FindNode(ax::mojom::Role::kGenericContainer, "rectB"); EXPECT_ACCESSIBILITY_HIT_TEST_RESULT(rect_b_point, expected_node, hit_node); + + // Try callback API. + hit_node = AsyncHitTestAndWaitForCallback(rect_b_point); + EXPECT_ACCESSIBILITY_HIT_TEST_RESULT(rect_b_point, expected_node, hit_node); } // Scroll div up 100px. @@ -468,6 +513,10 @@ IN_PROC_BROWSER_TEST_P(AccessibilityHitTestingCrossProcessBrowserTest, BrowserAccessibility* expected_node = FindNode(ax::mojom::Role::kGenericContainer, "rectG"); EXPECT_ACCESSIBILITY_HIT_TEST_RESULT(rect_g_point, expected_node, hit_node); + + // Try callback API. + hit_node = AsyncHitTestAndWaitForCallback(rect_g_point); + EXPECT_ACCESSIBILITY_HIT_TEST_RESULT(rect_g_point, expected_node, hit_node); } } @@ -595,6 +644,10 @@ IN_PROC_BROWSER_TEST_P(AccessibilityHitTestingBrowserTest, BrowserAccessibility* expected_node = FindNode(ax::mojom::Role::kGenericContainer, "rect2"); EXPECT_ACCESSIBILITY_HIT_TEST_RESULT(rect_2_point, expected_node, hit_node); + + // Try callback API. + hit_node = AsyncHitTestAndWaitForCallback(rect_2_point); + EXPECT_ACCESSIBILITY_HIT_TEST_RESULT(rect_2_point, expected_node, hit_node); } // Test a hit on a rect in the iframe. @@ -604,11 +657,23 @@ IN_PROC_BROWSER_TEST_P(AccessibilityHitTestingBrowserTest, BrowserAccessibility* expected_node = FindNode(ax::mojom::Role::kGenericContainer, "rectB"); EXPECT_ACCESSIBILITY_HIT_TEST_RESULT(rect_b_point, expected_node, hit_node); + + // Try callback API. + hit_node = AsyncHitTestAndWaitForCallback(rect_b_point); + EXPECT_ACCESSIBILITY_HIT_TEST_RESULT(rect_b_point, expected_node, hit_node); } } +// Timeouts on Linux. TODO(crbug.com/1083805): Enable this test. +#if defined(OS_LINUX) +#define MAYBE_CachingAsyncHitTestMissesElement_WithPinchZoom \ + DISABLED_CachingAsyncHitTestMissesElement_WithPinchZoom +#else +#define MAYBE_CachingAsyncHitTestMissesElement_WithPinchZoom \ + CachingAsyncHitTestMissesElement_WithPinchZoom +#endif IN_PROC_BROWSER_TEST_P(AccessibilityHitTestingBrowserTest, - CachingAsyncHitTestMissesElement_WithPinchZoom) { + MAYBE_CachingAsyncHitTestMissesElement_WithPinchZoom) { ASSERT_TRUE(embedded_test_server()->Start()); EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL))); diff --git a/chromium/content/browser/accessibility/hit_testing_browsertest.h b/chromium/content/browser/accessibility/hit_testing_browsertest.h index 05b294cef07..418d3f93dbd 100644 --- a/chromium/content/browser/accessibility/hit_testing_browsertest.h +++ b/chromium/content/browser/accessibility/hit_testing_browsertest.h @@ -35,11 +35,19 @@ class AccessibilityHitTestingBrowserTest gfx::Rect GetViewBoundsInScreenCoordinates(); gfx::Point CSSToFramePoint(gfx::Point css_point); gfx::Point CSSToPhysicalPixelPoint(gfx::Point css_point); + + // Test the hit test action that fires an event. BrowserAccessibility* HitTestAndWaitForResultWithEvent( const gfx::Point& point, ax::mojom::Event event_to_fire); BrowserAccessibility* HitTestAndWaitForResult(const gfx::Point& point); + + // Test the hit test mojo RPC that calls a callback function. + BrowserAccessibility* AsyncHitTestAndWaitForCallback(const gfx::Point& point); + + // Test the caching async hit test. BrowserAccessibility* CallCachingAsyncHitTest(const gfx::Point& page_point); + BrowserAccessibility* CallNearestLeafNode(const gfx::Point& page_point); void SynchronizeThreads(); base::string16 FormatHitTestAccessibilityTree(); diff --git a/chromium/content/browser/accessibility/hit_testing_mac_browsertest.mm b/chromium/content/browser/accessibility/hit_testing_mac_browsertest.mm new file mode 100644 index 00000000000..ba367164acb --- /dev/null +++ b/chromium/content/browser/accessibility/hit_testing_mac_browsertest.mm @@ -0,0 +1,82 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "content/browser/accessibility/browser_accessibility_cocoa.h" +#include "content/browser/accessibility/browser_accessibility_mac.h" +#include "content/browser/accessibility/hit_testing_browsertest.h" +#include "content/public/test/accessibility_notification_waiter.h" +#include "content/public/test/browser_test.h" +#include "content/public/test/content_browser_test_utils.h" +#include "content/shell/browser/shell.h" +#include "ui/gfx/mac/coordinate_conversion.h" + +namespace content { + +#define EXPECT_ACCESSIBILITY_MAC_HIT_TEST_RESULT(css_point, expected_element, \ + hit_element) \ + SCOPED_TRACE(GetScopedTrace(css_point)); \ + EXPECT_EQ([expected_element owner]->GetId(), [hit_element owner]->GetId()); + +class AccessibilityHitTestingMacBrowserTest + : public AccessibilityHitTestingBrowserTest { + public: + BrowserAccessibilityCocoa* GetWebContentRoot() { + return ToBrowserAccessibilityCocoa( + GetRootBrowserAccessibilityManager()->GetRoot()); + } +}; + +INSTANTIATE_TEST_SUITE_P( + All, + AccessibilityHitTestingMacBrowserTest, + ::testing::Combine(::testing::Values(1, 2), ::testing::Bool()), + AccessibilityHitTestingBrowserTest::TestPassToString()); + +IN_PROC_BROWSER_TEST_P(AccessibilityHitTestingMacBrowserTest, + AccessibilityHitTest) { + ASSERT_TRUE(embedded_test_server()->Start()); + + EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL))); + + AccessibilityNotificationWaiter waiter(shell()->web_contents(), + ui::kAXModeComplete, + ax::mojom::Event::kLoadComplete); + GURL url(embedded_test_server()->GetURL( + "/accessibility/hit_testing/simple_rectangles.html")); + EXPECT_TRUE(NavigateToURL(shell(), url)); + waiter.WaitForNotification(); + + WaitForAccessibilityTreeToContainNodeWithName(shell()->web_contents(), + "rectA"); + + BrowserAccessibilityCocoa* root = GetWebContentRoot(); + + // Test a hit on a rect in the main frame. + { + gfx::Point rect_2_point(49, 20); + gfx::Point rect_2_point_frame = CSSToFramePoint(rect_2_point); + BrowserAccessibilityCocoa* hit_element = + [root accessibilityHitTest:NSMakePoint(rect_2_point_frame.x(), + rect_2_point_frame.y())]; + BrowserAccessibilityCocoa* expected_element = ToBrowserAccessibilityCocoa( + FindNode(ax::mojom::Role::kGenericContainer, "rect2")); + EXPECT_ACCESSIBILITY_MAC_HIT_TEST_RESULT(rect_2_point, expected_element, + hit_element); + } + + // Test a hit on a rect in the iframe. + { + gfx::Point rect_b_point(79, 79); + gfx::Point rect_b_point_frame = CSSToFramePoint(rect_b_point); + BrowserAccessibilityCocoa* hit_element = + [root accessibilityHitTest:NSMakePoint(rect_b_point_frame.x(), + rect_b_point_frame.y())]; + BrowserAccessibilityCocoa* expected_element = ToBrowserAccessibilityCocoa( + FindNode(ax::mojom::Role::kGenericContainer, "rectB")); + EXPECT_ACCESSIBILITY_MAC_HIT_TEST_RESULT(rect_b_point, expected_element, + hit_element); + } +} + +} diff --git a/chromium/content/browser/accessibility/one_shot_accessibility_tree_search_unittest.cc b/chromium/content/browser/accessibility/one_shot_accessibility_tree_search_unittest.cc index c920deb7adc..db4cc2f2520 100644 --- a/chromium/content/browser/accessibility/one_shot_accessibility_tree_search_unittest.cc +++ b/chromium/content/browser/accessibility/one_shot_accessibility_tree_search_unittest.cc @@ -23,7 +23,7 @@ namespace { class TestBrowserAccessibilityManager : public BrowserAccessibilityManagerAndroid { public: - TestBrowserAccessibilityManager(const ui::AXTreeUpdate& initial_tree) + explicit TestBrowserAccessibilityManager(const ui::AXTreeUpdate& initial_tree) : BrowserAccessibilityManagerAndroid(initial_tree, nullptr, nullptr) {} }; #else @@ -55,15 +55,15 @@ class OneShotAccessibilityTreeSearchTest : public testing::TestWithParam<bool> { void OneShotAccessibilityTreeSearchTest::SetUp() { ui::AXNodeData root; root.id = 1; - root.SetName("Document"); root.role = ax::mojom::Role::kRootWebArea; + root.SetName("Document"); root.relative_bounds.bounds = gfx::RectF(0, 0, 800, 600); root.AddBoolAttribute(ax::mojom::BoolAttribute::kClipsChildren, true); ui::AXNodeData heading; heading.id = 2; - heading.SetName("Heading"); heading.role = ax::mojom::Role::kHeading; + heading.SetName("Heading"); heading.relative_bounds.bounds = gfx::RectF(0, 0, 800, 50); ui::AXNodeData table; @@ -102,20 +102,20 @@ void OneShotAccessibilityTreeSearchTest::SetUp() { ui::AXNodeData list_item_1; list_item_1.id = 8; - list_item_1.SetName("Autobots"); list_item_1.role = ax::mojom::Role::kListItem; + list_item_1.SetName("Autobots"); list_item_1.relative_bounds.bounds = gfx::RectF(10, 10, 200, 30); ui::AXNodeData list_item_2; list_item_2.id = 9; - list_item_2.SetName("Decepticons"); list_item_2.role = ax::mojom::Role::kListItem; + list_item_2.SetName("Decepticons"); list_item_2.relative_bounds.bounds = gfx::RectF(10, 40, 200, 60); ui::AXNodeData footer; footer.id = 10; - footer.SetName("Footer"); footer.role = ax::mojom::Role::kFooter; + footer.SetName("Footer"); footer.relative_bounds.bounds = gfx::RectF(0, 650, 100, 800); table_row.child_ids = {table_column_header_1.id, table_column_header_2.id}; diff --git a/chromium/content/browser/accessibility/site_per_process_accessibility_browsertest.cc b/chromium/content/browser/accessibility/site_per_process_accessibility_browsertest.cc index 6c60abaf02d..6b73b0ed4fe 100644 --- a/chromium/content/browser/accessibility/site_per_process_accessibility_browsertest.cc +++ b/chromium/content/browser/accessibility/site_per_process_accessibility_browsertest.cc @@ -29,6 +29,7 @@ #include "content/public/test/test_utils.h" #include "content/shell/browser/shell.h" #include "content/test/content_browser_test_utils_internal.h" +#include "content/test/render_document_feature.h" #include "net/dns/mock_host_resolver.h" #include "net/test/embedded_test_server/embedded_test_server.h" #include "url/gurl.h" @@ -42,6 +43,13 @@ #define MAYBE_SitePerProcessAccessibilityBrowserTest \ SitePerProcessAccessibilityBrowserTest #endif +// "All/DISABLED_SitePerProcessAccessibilityBrowserTest" does not work. We need +// "DISABLED_All/...". TODO(https://crbug.com/1096416) delete when fixed. +#if defined(OS_ANDROID) +#define MAYBE_All DISABLED_All +#else +#define MAYBE_All All +#endif namespace content { @@ -70,7 +78,7 @@ class MAYBE_SitePerProcessAccessibilityBrowserTest } }; -IN_PROC_BROWSER_TEST_F(MAYBE_SitePerProcessAccessibilityBrowserTest, +IN_PROC_BROWSER_TEST_P(MAYBE_SitePerProcessAccessibilityBrowserTest, CrossSiteIframeAccessibility) { // Enable full accessibility for all current and future WebContents. BrowserAccessibilityState::GetInstance()->EnableAccessibility(); @@ -138,7 +146,7 @@ IN_PROC_BROWSER_TEST_F(MAYBE_SitePerProcessAccessibilityBrowserTest, } // TODO(aboxhall): Flaky test, discuss with dmazzoni -IN_PROC_BROWSER_TEST_F(MAYBE_SitePerProcessAccessibilityBrowserTest, +IN_PROC_BROWSER_TEST_P(MAYBE_SitePerProcessAccessibilityBrowserTest, DISABLED_TwoCrossSiteNavigations) { // Enable full accessibility for all current and future WebContents. BrowserAccessibilityState::GetInstance()->EnableAccessibility(); @@ -168,7 +176,7 @@ IN_PROC_BROWSER_TEST_F(MAYBE_SitePerProcessAccessibilityBrowserTest, // Ensure that enabling accessibility and doing a remote-to-local main frame // navigation doesn't crash. See https://crbug.com/762824. -IN_PROC_BROWSER_TEST_F(MAYBE_SitePerProcessAccessibilityBrowserTest, +IN_PROC_BROWSER_TEST_P(MAYBE_SitePerProcessAccessibilityBrowserTest, RemoteToLocalMainFrameNavigation) { // Enable full accessibility for all current and future WebContents. BrowserAccessibilityState::GetInstance()->EnableAccessibility(); @@ -192,4 +200,7 @@ IN_PROC_BROWSER_TEST_F(MAYBE_SitePerProcessAccessibilityBrowserTest, "Title Of Awesomeness"); } +INSTANTIATE_TEST_SUITE_P(MAYBE_All, + MAYBE_SitePerProcessAccessibilityBrowserTest, + testing::ValuesIn(RenderDocumentFeatureLevelValues())); } // namespace content diff --git a/chromium/content/browser/accessibility/snapshot_ax_tree_browsertest.cc b/chromium/content/browser/accessibility/snapshot_ax_tree_browsertest.cc index 6d1a247582b..c807616350c 100644 --- a/chromium/content/browser/accessibility/snapshot_ax_tree_browsertest.cc +++ b/chromium/content/browser/accessibility/snapshot_ax_tree_browsertest.cc @@ -270,4 +270,102 @@ IN_PROC_BROWSER_TEST_F(SnapshotAXTreeBrowserTest, total_attribute_count(complete_nodes[i])); } +IN_PROC_BROWSER_TEST_F(SnapshotAXTreeBrowserTest, SnapshotPDFMode) { + // The "PDF" accessibility mode is used when getting a snapshot of the + // accessibility tree in order to export a tagged PDF. Ensure that + // we're serializing the right set of attributes needed for a PDF and + // also ensure that we're *not* wasting time serializing attributes + // that are not needed for PDF export. + GURL url(R"HTML(data:text/html,<body> + <img src="" alt="Unicorns"> + <ul> + <li aria-posinset="5"> + <span style="color: red;">Red text</span> + </ul> + <table role="table"> + <tr> + <td colspan="2"> + </tr> + <tr> + <td>1</td><td>2</td> + </tr> + </table> + </body>)HTML"); + EXPECT_TRUE(NavigateToURL(shell(), url)); + + auto* web_contents = static_cast<WebContentsImpl*>(shell()->web_contents()); + AXTreeSnapshotWaiter waiter; + web_contents->RequestAXTreeSnapshot( + base::BindOnce(&AXTreeSnapshotWaiter::ReceiveSnapshot, + base::Unretained(&waiter)), + ui::AXMode::kPDF); + waiter.Wait(); + + // Dump the whole tree if one of the assertions below fails + // to aid in debugging why it failed. + SCOPED_TRACE(waiter.snapshot().ToString()); + + // Scan all of the nodes and make some general assertions. + int dom_node_id_count = 0; + for (const ui::AXNodeData& node_data : waiter.snapshot().nodes) { + // Every node should have a valid role, state, and ID. + EXPECT_NE(ax::mojom::Role::kUnknown, node_data.role); + EXPECT_NE(0, node_data.id); + + if (node_data.GetIntAttribute(ax::mojom::IntAttribute::kDOMNodeId) != 0) + dom_node_id_count++; + + // We don't need bounding boxes to make a tagged PDF. Ensure those are + // uninitialized. + EXPECT_TRUE(node_data.relative_bounds.bounds.IsEmpty()); + + // We shouldn't get any inline text box nodes. They aren't needed to + // make a tagged PDF and they make up a large fraction of nodes in the + // tree when present. + EXPECT_NE(ax::mojom::Role::kInlineTextBox, node_data.role); + + // We shouldn't have any style information like color in the tree. + EXPECT_FALSE(node_data.HasIntAttribute(ax::mojom::IntAttribute::kColor)); + } + + // Many nodes should have a DOM node id. That's not normally included + // in the accessibility tree but it's needed for associating nodes with + // rendered text in the PDF file. + EXPECT_GT(dom_node_id_count, 5); + + // Build an AXTree from the snapshot and make some specific assertions. + ui::AXTree tree(waiter.snapshot()); + ui::AXNode* root = tree.root(); + ASSERT_TRUE(root); + ASSERT_EQ(ax::mojom::Role::kRootWebArea, root->data().role); + + // Img alt text should be present. + ui::AXNode* image = root->GetUnignoredChildAtIndex(0); + ASSERT_TRUE(image); + ASSERT_EQ(ax::mojom::Role::kImage, image->data().role); + ASSERT_EQ("Unicorns", image->data().GetStringAttribute( + ax::mojom::StringAttribute::kName)); + + // List attributes like posinset should be present. + ui::AXNode* ul = root->GetUnignoredChildAtIndex(1); + ASSERT_TRUE(ul); + ASSERT_EQ(ax::mojom::Role::kList, ul->data().role); + ui::AXNode* li = ul->GetUnignoredChildAtIndex(0); + ASSERT_TRUE(li); + ASSERT_EQ(ax::mojom::Role::kListItem, li->data().role); + EXPECT_EQ(5, *li->GetPosInSet()); + + // Table attributes like colspan should be present. + ui::AXNode* table = root->GetUnignoredChildAtIndex(2); + ASSERT_TRUE(table); + ASSERT_EQ(ax::mojom::Role::kTable, table->data().role); + ui::AXNode* tr = table->GetUnignoredChildAtIndex(0); + ASSERT_TRUE(tr); + ASSERT_EQ(ax::mojom::Role::kRow, tr->data().role); + ui::AXNode* td = tr->GetUnignoredChildAtIndex(0); + ASSERT_TRUE(td); + ASSERT_EQ(ax::mojom::Role::kCell, td->data().role); + EXPECT_EQ(2, *td->GetTableCellColSpan()); +} + } // namespace content diff --git a/chromium/content/browser/accessibility/test_browser_accessibility_delegate.cc b/chromium/content/browser/accessibility/test_browser_accessibility_delegate.cc index 9e3fe4f42ca..72d1d2610ad 100644 --- a/chromium/content/browser/accessibility/test_browser_accessibility_delegate.cc +++ b/chromium/content/browser/accessibility/test_browser_accessibility_delegate.cc @@ -55,6 +55,13 @@ bool TestBrowserAccessibilityDelegate::AccessibilityIsMainFrame() { return is_root_frame_; } +void TestBrowserAccessibilityDelegate::AccessibilityHitTest( + const gfx::Point& point_in_frame_pixels, + ax::mojom::Event opt_event_to_fire, + int opt_request_id, + base::OnceCallback<void(BrowserAccessibilityManager* hit_manager, + int hit_node_id)> opt_callback) {} + bool TestBrowserAccessibilityDelegate::got_fatal_error() const { return got_fatal_error_; } diff --git a/chromium/content/browser/accessibility/test_browser_accessibility_delegate.h b/chromium/content/browser/accessibility/test_browser_accessibility_delegate.h index 818e827bd32..5b9b2607fe8 100644 --- a/chromium/content/browser/accessibility/test_browser_accessibility_delegate.h +++ b/chromium/content/browser/accessibility/test_browser_accessibility_delegate.h @@ -25,6 +25,12 @@ class TestBrowserAccessibilityDelegate : public BrowserAccessibilityDelegate { override; WebContents* AccessibilityWebContents() override; bool AccessibilityIsMainFrame() override; + void AccessibilityHitTest( + const gfx::Point& point_in_frame_pixels, + ax::mojom::Event opt_event_to_fire, + int opt_request_id, + base::OnceCallback<void(BrowserAccessibilityManager* hit_manager, + int hit_node_id)> opt_callback) override; bool got_fatal_error() const; void reset_got_fatal_error(); diff --git a/chromium/content/browser/accessibility/web_contents_accessibility_android.cc b/chromium/content/browser/accessibility/web_contents_accessibility_android.cc index 4633b8e274d..b53ca692ff8 100644 --- a/chromium/content/browser/accessibility/web_contents_accessibility_android.cc +++ b/chromium/content/browser/accessibility/web_contents_accessibility_android.cc @@ -697,13 +697,13 @@ void WebContentsAccessibilityAndroid::UpdateAccessibilityNodeInfoBoundsRect( gfx::Rect absolute_rect = gfx::ScaleToEnclosingRect( node->GetUnclippedRootFrameBoundsRect(), dip_scale, dip_scale); gfx::Rect parent_relative_rect = absolute_rect; - if (node->PlatformGetParent()) { + bool is_root = node->PlatformGetParent() == nullptr; + if (!is_root) { gfx::Rect parent_rect = gfx::ScaleToEnclosingRect( node->PlatformGetParent()->GetUnclippedRootFrameBoundsRect(), dip_scale, dip_scale); parent_relative_rect.Offset(-parent_rect.OffsetFromOrigin()); } - bool is_root = node->PlatformGetParent() == NULL; Java_WebContentsAccessibilityImpl_setAccessibilityNodeInfoLocation( env, obj, info, unique_id, absolute_rect.x(), absolute_rect.y(), parent_relative_rect.x(), parent_relative_rect.y(), absolute_rect.width(), @@ -734,7 +734,8 @@ jboolean WebContentsAccessibilityAndroid::PopulateAccessibilityNodeInfo( if (!node) return false; - if (node->PlatformGetParent()) { + bool is_root = node->PlatformGetParent() == nullptr; + if (!is_root) { auto* android_node = static_cast<BrowserAccessibilityAndroid*>(node->PlatformGetParent()); Java_WebContentsAccessibilityImpl_setAccessibilityNodeInfoParent( @@ -749,9 +750,10 @@ jboolean WebContentsAccessibilityAndroid::PopulateAccessibilityNodeInfo( } Java_WebContentsAccessibilityImpl_setAccessibilityNodeInfoBooleanAttributes( env, obj, info, unique_id, node->IsCheckable(), node->IsChecked(), - node->IsClickable(), node->IsEnabled(), node->IsFocusable(), - node->IsFocused(), node->IsPasswordField(), node->IsScrollable(), - node->IsSelected(), node->IsVisibleToUser()); + node->IsClickable(), node->IsContentInvalid(), node->IsEnabled(), + node->IsFocusable(), node->IsFocused(), node->HasImage(), + node->IsPasswordField(), node->IsScrollable(), node->IsSelected(), + node->IsVisibleToUser()); Java_WebContentsAccessibilityImpl_addAccessibilityNodeInfoActions( env, obj, info, unique_id, node->CanScrollForward(), node->CanScrollBackward(), node->CanScrollUp(), node->CanScrollDown(), @@ -760,9 +762,14 @@ jboolean WebContentsAccessibilityAndroid::PopulateAccessibilityNodeInfo( node->IsFocused(), node->IsCollapsed(), node->IsExpanded(), node->HasNonEmptyValue(), !node->GetInnerText().empty(), node->IsRangeType(), node->IsFormDescendant()); - Java_WebContentsAccessibilityImpl_setAccessibilityNodeInfoClassName( - env, obj, info, - base::android::ConvertUTF8ToJavaString(env, node->GetClassName())); + + Java_WebContentsAccessibilityImpl_setAccessibilityNodeInfoBaseAttributes( + env, obj, info, is_root, + base::android::ConvertUTF8ToJavaString(env, node->GetClassName()), + base::android::ConvertUTF8ToJavaString(env, node->GetRoleString()), + base::android::ConvertUTF16ToJavaString(env, node->GetRoleDescription()), + base::android::ConvertUTF16ToJavaString(env, node->GetHint()), + base::android::ConvertUTF16ToJavaString(env, node->GetTargetUrl())); ScopedJavaLocalRef<jintArray> suggestion_starts_java; ScopedJavaLocalRef<jintArray> suggestion_ends_java; @@ -801,18 +808,6 @@ jboolean WebContentsAccessibilityAndroid::PopulateAccessibilityNodeInfo( UpdateAccessibilityNodeInfoBoundsRect(env, obj, info, unique_id, node); - bool is_root = node->PlatformGetParent() == NULL; - - Java_WebContentsAccessibilityImpl_setAccessibilityNodeInfoKitKatAttributes( - env, obj, info, is_root, node->IsTextField(), - base::android::ConvertUTF8ToJavaString(env, node->GetRoleString()), - base::android::ConvertUTF16ToJavaString(env, node->GetRoleDescription()), - base::android::ConvertUTF16ToJavaString(env, node->GetHint()), - node->GetIntAttribute(ax::mojom::IntAttribute::kTextSelStart), - node->GetIntAttribute(ax::mojom::IntAttribute::kTextSelEnd), - node->HasImage(), node->IsContentInvalid(), - base::android::ConvertUTF16ToJavaString(env, node->GetTargetUrl())); - Java_WebContentsAccessibilityImpl_setAccessibilityNodeInfoLollipopAttributes( env, obj, info, node->CanOpenPopup(), node->IsContentInvalid(), node->IsDismissable(), node->IsMultiLine(), node->AndroidInputType(), @@ -820,9 +815,9 @@ jboolean WebContentsAccessibilityAndroid::PopulateAccessibilityNodeInfo( base::android::ConvertUTF16ToJavaString( env, node->GetContentInvalidErrorMessage())); - bool has_character_locations = node->HasCharacterLocations(); Java_WebContentsAccessibilityImpl_setAccessibilityNodeInfoOAttributes( - env, obj, info, has_character_locations); + env, obj, info, node->HasCharacterLocations(), + base::android::ConvertUTF16ToJavaString(env, node->GetHint())); if (node->IsCollection()) { Java_WebContentsAccessibilityImpl_setAccessibilityNodeInfoCollectionInfo( @@ -846,6 +841,11 @@ jboolean WebContentsAccessibilityAndroid::PopulateAccessibilityNodeInfo( base::android::ConvertUTF16ToJavaString(env, node->GetInnerText())); } + if (node->IsTextField()) { + Java_WebContentsAccessibilityImpl_setAccessibilityNodeInfoSelectionAttrs( + env, obj, info, node->GetSelectionStart(), node->GetSelectionEnd()); + } + return true; } |