| // Copyright 2017 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 "core/dom/AccessibleNode.h" |
| #include "core/dom/AccessibleNodeList.h" |
| #include "core/html/HTMLBodyElement.h" |
| #include "core/testing/sim/SimRequest.h" |
| #include "core/testing/sim/SimTest.h" |
| #include "modules/accessibility/AXObjectCacheImpl.h" |
| #include "modules/accessibility/AXTable.h" |
| #include "modules/accessibility/AXTableCell.h" |
| #include "modules/accessibility/AXTableRow.h" |
| #include "platform/testing/RuntimeEnabledFeaturesTestHelpers.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| class AccessibilityObjectModelTest |
| : public SimTest, |
| public ScopedAccessibilityObjectModelForTest { |
| public: |
| AccessibilityObjectModelTest() |
| : ScopedAccessibilityObjectModelForTest(true) {} |
| |
| protected: |
| AXObjectCacheImpl* AXObjectCache() { |
| GetDocument().GetSettings()->SetAccessibilityEnabled(true); |
| return static_cast<AXObjectCacheImpl*>( |
| GetDocument().GetOrCreateAXObjectCache()); |
| } |
| }; |
| |
| TEST_F(AccessibilityObjectModelTest, DOMElementsHaveAnAccessibleNode) { |
| SimRequest main_resource("https://example.com/", "text/html"); |
| LoadURL("https://example.com/"); |
| main_resource.Complete("<button id=button>Click me</button>"); |
| |
| auto* button = GetDocument().getElementById("button"); |
| EXPECT_NE(nullptr, button->accessibleNode()); |
| EXPECT_TRUE(button->accessibleNode()->role().IsNull()); |
| EXPECT_TRUE(button->accessibleNode()->label().IsNull()); |
| } |
| |
| TEST_F(AccessibilityObjectModelTest, SetAccessibleNodeRole) { |
| SimRequest main_resource("https://example.com/", "text/html"); |
| LoadURL("https://example.com/"); |
| main_resource.Complete("<button id=button>Click me</button>"); |
| |
| auto* cache = AXObjectCache(); |
| ASSERT_NE(nullptr, cache); |
| |
| auto* button = GetDocument().getElementById("button"); |
| ASSERT_NE(nullptr, button); |
| |
| auto* axButton = cache->GetOrCreate(button); |
| EXPECT_EQ(kButtonRole, axButton->RoleValue()); |
| |
| button->accessibleNode()->setRole("slider"); |
| EXPECT_EQ("slider", button->accessibleNode()->role()); |
| |
| axButton = cache->GetOrCreate(button); |
| EXPECT_EQ(kSliderRole, axButton->RoleValue()); |
| } |
| |
| TEST_F(AccessibilityObjectModelTest, AOMDoesNotReflectARIA) { |
| SimRequest main_resource("https://example.com/", "text/html"); |
| LoadURL("https://example.com/"); |
| main_resource.Complete("<input id=textbox>"); |
| |
| // Set ARIA attributes. |
| auto* textbox = GetDocument().getElementById("textbox"); |
| ASSERT_NE(nullptr, textbox); |
| textbox->setAttribute("role", "combobox"); |
| textbox->setAttribute("aria-label", "Combo"); |
| textbox->setAttribute("aria-disabled", "true"); |
| |
| // Assert that the ARIA attributes affect the AX object. |
| auto* cache = AXObjectCache(); |
| ASSERT_NE(nullptr, cache); |
| auto* axTextBox = cache->GetOrCreate(textbox); |
| EXPECT_EQ(kTextFieldWithComboBoxRole, axTextBox->RoleValue()); |
| AXNameFrom name_from; |
| AXObject::AXObjectVector name_objects; |
| EXPECT_EQ("Combo", axTextBox->GetName(name_from, &name_objects)); |
| EXPECT_EQ(axTextBox->Restriction(), kDisabled); |
| |
| // The AOM properties should still all be null. |
| EXPECT_EQ(nullptr, textbox->accessibleNode()->role()); |
| EXPECT_EQ(nullptr, textbox->accessibleNode()->label()); |
| bool is_null = false; |
| EXPECT_FALSE(textbox->accessibleNode()->disabled(is_null)); |
| EXPECT_TRUE(is_null); |
| } |
| |
| TEST_F(AccessibilityObjectModelTest, AOMPropertiesCanBeCleared) { |
| SimRequest main_resource("https://example.com/", "text/html"); |
| LoadURL("https://example.com/"); |
| main_resource.Complete("<input type=button id=button>"); |
| |
| // Set ARIA attributes. |
| auto* button = GetDocument().getElementById("button"); |
| ASSERT_NE(nullptr, button); |
| button->setAttribute("role", "checkbox"); |
| button->setAttribute("aria-label", "Check"); |
| button->setAttribute("aria-disabled", "true"); |
| |
| // Assert that the AX object was affected by ARIA attributes. |
| auto* cache = AXObjectCache(); |
| ASSERT_NE(nullptr, cache); |
| auto* axButton = cache->GetOrCreate(button); |
| EXPECT_EQ(kCheckBoxRole, axButton->RoleValue()); |
| AXNameFrom name_from; |
| AXObject::AXObjectVector name_objects; |
| EXPECT_EQ("Check", axButton->GetName(name_from, &name_objects)); |
| EXPECT_EQ(axButton->Restriction(), kDisabled); |
| |
| // Now set the AOM properties to override. |
| button->accessibleNode()->setRole("radio"); |
| button->accessibleNode()->setLabel("Radio"); |
| button->accessibleNode()->setDisabled(false, false); |
| |
| // Assert that the AX object was affected by AOM properties. |
| axButton = cache->GetOrCreate(button); |
| EXPECT_EQ(kRadioButtonRole, axButton->RoleValue()); |
| EXPECT_EQ("Radio", axButton->GetName(name_from, &name_objects)); |
| EXPECT_EQ(axButton->Restriction(), kNone); |
| |
| // Null the AOM properties. |
| button->accessibleNode()->setRole(g_null_atom); |
| button->accessibleNode()->setLabel(g_null_atom); |
| button->accessibleNode()->setDisabled(false, true); |
| |
| // The AX Object should now revert to ARIA. |
| axButton = cache->GetOrCreate(button); |
| EXPECT_EQ(kCheckBoxRole, axButton->RoleValue()); |
| EXPECT_EQ("Check", axButton->GetName(name_from, &name_objects)); |
| EXPECT_EQ(axButton->Restriction(), kDisabled); |
| } |
| |
| TEST_F(AccessibilityObjectModelTest, RangeProperties) { |
| SimRequest main_resource("https://example.com/", "text/html"); |
| LoadURL("https://example.com/"); |
| main_resource.Complete("<div role=slider id=slider>"); |
| |
| auto* slider = GetDocument().getElementById("slider"); |
| ASSERT_NE(nullptr, slider); |
| slider->accessibleNode()->setValueMin(-0.5, false); |
| slider->accessibleNode()->setValueMax(0.5, false); |
| slider->accessibleNode()->setValueNow(0.1, false); |
| |
| auto* cache = AXObjectCache(); |
| ASSERT_NE(nullptr, cache); |
| auto* ax_slider = cache->GetOrCreate(slider); |
| float value = 0.0f; |
| EXPECT_TRUE(ax_slider->MinValueForRange(&value)); |
| EXPECT_EQ(-0.5f, value); |
| EXPECT_TRUE(ax_slider->MaxValueForRange(&value)); |
| EXPECT_EQ(0.5f, value); |
| EXPECT_TRUE(ax_slider->ValueForRange(&value)); |
| EXPECT_EQ(0.1f, value); |
| } |
| |
| TEST_F(AccessibilityObjectModelTest, Level) { |
| SimRequest main_resource("https://example.com/", "text/html"); |
| LoadURL("https://example.com/"); |
| main_resource.Complete("<div role=heading id=heading>"); |
| |
| auto* heading = GetDocument().getElementById("heading"); |
| ASSERT_NE(nullptr, heading); |
| heading->accessibleNode()->setLevel(5, false); |
| |
| auto* cache = AXObjectCache(); |
| ASSERT_NE(nullptr, cache); |
| auto* ax_heading = cache->GetOrCreate(heading); |
| EXPECT_EQ(5, ax_heading->HeadingLevel()); |
| } |
| |
| TEST_F(AccessibilityObjectModelTest, ListItem) { |
| SimRequest main_resource("https://example.com/", "text/html"); |
| LoadURL("https://example.com/"); |
| main_resource.Complete( |
| "<div role=list><div role=listitem id=listitem></div></div>"); |
| |
| auto* listitem = GetDocument().getElementById("listitem"); |
| ASSERT_NE(nullptr, listitem); |
| listitem->accessibleNode()->setPosInSet(9, false); |
| listitem->accessibleNode()->setSetSize(10, false); |
| |
| auto* cache = AXObjectCache(); |
| ASSERT_NE(nullptr, cache); |
| auto* ax_listitem = cache->GetOrCreate(listitem); |
| EXPECT_EQ(9, ax_listitem->PosInSet()); |
| EXPECT_EQ(10, ax_listitem->SetSize()); |
| } |
| |
| TEST_F(AccessibilityObjectModelTest, Grid) { |
| SimRequest main_resource("https://example.com/", "text/html"); |
| LoadURL("https://example.com/"); |
| main_resource.Complete(R"HTML( |
| <div role=grid id=grid> |
| <div role=row id=row> |
| <div role=gridcell id=cell></div> |
| <div role=gridcell id=cell2></div> |
| </div> |
| </div> |
| )HTML"); |
| |
| auto* grid = GetDocument().getElementById("grid"); |
| ASSERT_NE(nullptr, grid); |
| grid->accessibleNode()->setColCount(16, false); |
| grid->accessibleNode()->setRowCount(9, false); |
| |
| auto* row = GetDocument().getElementById("row"); |
| ASSERT_NE(nullptr, row); |
| row->accessibleNode()->setColIndex(8, false); |
| row->accessibleNode()->setRowIndex(5, false); |
| |
| auto* cell = GetDocument().getElementById("cell"); |
| |
| auto* cell2 = GetDocument().getElementById("cell2"); |
| ASSERT_NE(nullptr, cell2); |
| cell2->accessibleNode()->setColIndex(10, false); |
| cell2->accessibleNode()->setRowIndex(7, false); |
| |
| auto* cache = AXObjectCache(); |
| ASSERT_NE(nullptr, cache); |
| |
| auto* ax_grid = static_cast<AXTable*>(cache->GetOrCreate(grid)); |
| EXPECT_EQ(16, ax_grid->AriaColumnCount()); |
| EXPECT_EQ(9, ax_grid->AriaRowCount()); |
| |
| auto* ax_cell = static_cast<AXTableCell*>(cache->GetOrCreate(cell)); |
| EXPECT_TRUE(ax_cell->IsTableCell()); |
| EXPECT_EQ(8U, ax_cell->AriaColumnIndex()); |
| EXPECT_EQ(5U, ax_cell->AriaRowIndex()); |
| |
| auto* ax_cell2 = static_cast<AXTableCell*>(cache->GetOrCreate(cell2)); |
| EXPECT_TRUE(ax_cell2->IsTableCell()); |
| EXPECT_EQ(10U, ax_cell2->AriaColumnIndex()); |
| EXPECT_EQ(7U, ax_cell2->AriaRowIndex()); |
| } |
| |
| class SparseAttributeAdapter : public AXSparseAttributeClient { |
| public: |
| SparseAttributeAdapter() = default; |
| |
| std::map<AXBoolAttribute, bool> bool_attributes; |
| std::map<AXStringAttribute, String> string_attributes; |
| std::map<AXObjectAttribute, Persistent<AXObject>> object_attributes; |
| std::map<AXObjectVectorAttribute, HeapVector<Member<AXObject>>> |
| object_vector_attributes; |
| |
| private: |
| void AddBoolAttribute(AXBoolAttribute attribute, bool value) override { |
| ASSERT_TRUE(bool_attributes.find(attribute) == bool_attributes.end()); |
| bool_attributes[attribute] = value; |
| } |
| |
| void AddStringAttribute(AXStringAttribute attribute, |
| const String& value) override { |
| ASSERT_TRUE(string_attributes.find(attribute) == string_attributes.end()); |
| string_attributes[attribute] = value; |
| } |
| |
| void AddObjectAttribute(AXObjectAttribute attribute, |
| AXObject& value) override { |
| ASSERT_TRUE(object_attributes.find(attribute) == object_attributes.end()); |
| object_attributes[attribute] = value; |
| } |
| |
| void AddObjectVectorAttribute(AXObjectVectorAttribute attribute, |
| HeapVector<Member<AXObject>>& value) override { |
| ASSERT_TRUE(object_vector_attributes.find(attribute) == |
| object_vector_attributes.end()); |
| object_vector_attributes[attribute] = value; |
| } |
| }; |
| |
| TEST_F(AccessibilityObjectModelTest, SparseAttributes) { |
| SimRequest main_resource("https://example.com/", "text/html"); |
| LoadURL("https://example.com/"); |
| main_resource.Complete(R"HTML( |
| <input id=target |
| aria-keyshortcuts=Ctrl+K |
| aria-roledescription=Widget |
| aria-activedescendant=active |
| aria-details=details |
| aria-errormessage=error> |
| <div id=active role=option></div> |
| <div id=active2 role=gridcell></div> |
| <div id=details role=contentinfo></div> |
| <div id=details2 role=form></div> |
| <div id=error role=article>Error</div> |
| <div id=error2 role=banner>Error 2</div> |
| )HTML"); |
| |
| auto* target = GetDocument().getElementById("target"); |
| auto* cache = AXObjectCache(); |
| ASSERT_NE(nullptr, cache); |
| auto* ax_target = cache->GetOrCreate(target); |
| SparseAttributeAdapter sparse_attributes; |
| ax_target->GetSparseAXAttributes(sparse_attributes); |
| |
| ASSERT_EQ("Ctrl+K", |
| sparse_attributes |
| .string_attributes[AXStringAttribute::kAriaKeyShortcuts]); |
| ASSERT_EQ("Widget", |
| sparse_attributes |
| .string_attributes[AXStringAttribute::kAriaRoleDescription]); |
| ASSERT_EQ(kListBoxOptionRole, |
| sparse_attributes |
| .object_attributes[AXObjectAttribute::kAriaActiveDescendant] |
| ->RoleValue()); |
| ASSERT_EQ( |
| kContentInfoRole, |
| sparse_attributes.object_attributes[AXObjectAttribute::kAriaDetails] |
| ->RoleValue()); |
| ASSERT_EQ( |
| kArticleRole, |
| sparse_attributes.object_attributes[AXObjectAttribute::kAriaErrorMessage] |
| ->RoleValue()); |
| |
| target->accessibleNode()->setKeyShortcuts("Ctrl+L"); |
| target->accessibleNode()->setRoleDescription("Object"); |
| target->accessibleNode()->setActiveDescendant( |
| GetDocument().getElementById("active2")->accessibleNode()); |
| target->accessibleNode()->setDetails( |
| GetDocument().getElementById("details2")->accessibleNode()); |
| target->accessibleNode()->setErrorMessage( |
| GetDocument().getElementById("error2")->accessibleNode()); |
| |
| SparseAttributeAdapter sparse_attributes2; |
| ax_target->GetSparseAXAttributes(sparse_attributes2); |
| |
| ASSERT_EQ("Ctrl+L", |
| sparse_attributes2 |
| .string_attributes[AXStringAttribute::kAriaKeyShortcuts]); |
| ASSERT_EQ("Object", |
| sparse_attributes2 |
| .string_attributes[AXStringAttribute::kAriaRoleDescription]); |
| ASSERT_EQ(kCellRole, |
| sparse_attributes2 |
| .object_attributes[AXObjectAttribute::kAriaActiveDescendant] |
| ->RoleValue()); |
| ASSERT_EQ(kFormRole, sparse_attributes2 |
| .object_attributes[AXObjectAttribute::kAriaDetails] |
| ->RoleValue()); |
| ASSERT_EQ(kBannerRole, |
| sparse_attributes2 |
| .object_attributes[AXObjectAttribute::kAriaErrorMessage] |
| ->RoleValue()); |
| } |
| |
| TEST_F(AccessibilityObjectModelTest, LabeledBy) { |
| SimRequest main_resource("https://example.com/", "text/html"); |
| LoadURL("https://example.com/"); |
| main_resource.Complete(R"HTML( |
| <input id=target aria-labelledby='l1 l2'> |
| <label id=l1>Label 1</label> |
| <label id=l2>Label 2</label> |
| <label id=l3>Label 3</label> |
| )HTML"); |
| |
| auto* target = GetDocument().getElementById("target"); |
| auto* l1 = GetDocument().getElementById("l1"); |
| auto* l2 = GetDocument().getElementById("l2"); |
| auto* l3 = GetDocument().getElementById("l3"); |
| |
| HeapVector<Member<Element>> labeled_by; |
| ASSERT_TRUE(AccessibleNode::GetPropertyOrARIAAttribute( |
| target, AOMRelationListProperty::kLabeledBy, labeled_by)); |
| ASSERT_EQ(2U, labeled_by.size()); |
| ASSERT_EQ(l1, labeled_by[0]); |
| ASSERT_EQ(l2, labeled_by[1]); |
| |
| AccessibleNodeList* node_list = target->accessibleNode()->labeledBy(); |
| ASSERT_EQ(nullptr, node_list); |
| |
| node_list = new AccessibleNodeList(); |
| node_list->add(l3->accessibleNode()); |
| target->accessibleNode()->setLabeledBy(node_list); |
| |
| labeled_by.clear(); |
| ASSERT_TRUE(AccessibleNode::GetPropertyOrARIAAttribute( |
| target, AOMRelationListProperty::kLabeledBy, labeled_by)); |
| ASSERT_EQ(1U, labeled_by.size()); |
| ASSERT_EQ(l3, labeled_by[0]); |
| } |
| |
| } // namespace |
| |
| } // namespace blink |