blob: 218c43d12352a32d727854daec4783fd707ecaa6 [file] [log] [blame]
// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/core/accessibility/ax_context.h"
#include "third_party/blink/renderer/core/aom/accessible_node.h"
#include "third_party/blink/renderer/core/aom/accessible_node_list.h"
#include "third_party/blink/renderer/core/html/html_body_element.h"
#include "third_party/blink/renderer/core/testing/sim/sim_request.h"
#include "third_party/blink/renderer/core/testing/sim/sim_test.h"
#include "third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.h"
#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
#include "third_party/blink/renderer/platform/wtf/hash_map.h"
#include "ui/accessibility/ax_mode.h"
namespace blink {
namespace test {
namespace {
class AccessibilityObjectModelTest
: public SimTest,
public ScopedAccessibilityObjectModelForTest {
public:
AccessibilityObjectModelTest()
: ScopedAccessibilityObjectModelForTest(true) {}
protected:
AXObjectCacheImpl* AXObjectCache() {
return static_cast<AXObjectCacheImpl*>(
GetDocument().ExistingAXObjectCache());
}
};
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>");
AXContext ax_context(GetDocument(), ui::kAXModeComplete);
auto* button = GetDocument().getElementById(AtomicString("button"));
EXPECT_NE(nullptr, button->accessibleNode());
EXPECT_TRUE(button->accessibleNode()->role().IsNull());
EXPECT_TRUE(button->accessibleNode()->label().IsNull());
}
// AccessibleNode is being refactored to remove it's ability to modify the
// underlying accessibility tree. This test has been modified to assert that no
// changes in corresponding AXObjects are observed, but will likely be removed
// in the future.
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>");
AXContext ax_context(GetDocument(), ui::kAXModeComplete);
auto* cache = AXObjectCache();
ASSERT_NE(nullptr, cache);
cache->UpdateAXForAllDocuments();
auto* button = GetDocument().getElementById(AtomicString("button"));
ASSERT_NE(nullptr, button);
auto* axButton = cache->Get(button);
EXPECT_EQ(ax::mojom::Role::kButton, axButton->RoleValue());
button->accessibleNode()->setRole(AtomicString("slider"));
EXPECT_EQ("slider", button->accessibleNode()->role());
GetDocument().View()->UpdateLifecycleToLayoutClean(
DocumentUpdateReason::kTest);
axButton = cache->Get(button);
// No change in the AXObject role should be observed.
EXPECT_EQ(ax::mojom::Role::kButton, axButton->RoleValue());
}
TEST_F(AccessibilityObjectModelTest, AOMDoesNotReflectARIA) {
SimRequest main_resource("https://example.com/", "text/html");
LoadURL("https://example.com/");
main_resource.Complete("<input id=textbox>");
AXContext ax_context(GetDocument(), ui::kAXModeComplete);
// Set ARIA attributes.
auto* textbox = GetDocument().getElementById(AtomicString("textbox"));
ASSERT_NE(nullptr, textbox);
textbox->setAttribute(html_names::kRoleAttr, AtomicString("combobox"));
textbox->setAttribute(html_names::kAriaLabelAttr, AtomicString("Combo"));
textbox->setAttribute(html_names::kAriaDisabledAttr, AtomicString("true"));
// Assert that the ARIA attributes affect the AX object.
auto* cache = AXObjectCache();
ASSERT_NE(nullptr, cache);
cache->UpdateAXForAllDocuments();
auto* axTextBox = cache->Get(textbox);
EXPECT_EQ(ax::mojom::Role::kTextFieldWithComboBox, axTextBox->RoleValue());
ax::mojom::NameFrom name_from;
AXObject::AXObjectVector name_objects;
EXPECT_EQ("Combo", axTextBox->GetName(name_from, &name_objects));
EXPECT_EQ(axTextBox->Restriction(), kRestrictionDisabled);
// The AOM properties should still all be null.
EXPECT_EQ(nullptr, textbox->accessibleNode()->role());
EXPECT_EQ(nullptr, textbox->accessibleNode()->label());
EXPECT_FALSE(textbox->accessibleNode()->disabled().has_value());
}
TEST_F(AccessibilityObjectModelTest, AOMPropertiesCanBeCleared) {
SimRequest main_resource("https://example.com/", "text/html");
LoadURL("https://example.com/");
main_resource.Complete("<input type=button id=button>");
AXContext ax_context(GetDocument(), ui::kAXModeComplete);
// Set ARIA attributes.
auto* button = GetDocument().getElementById(AtomicString("button"));
ASSERT_NE(nullptr, button);
button->setAttribute(html_names::kRoleAttr, AtomicString("checkbox"));
button->setAttribute(html_names::kAriaLabelAttr, AtomicString("Check"));
button->setAttribute(html_names::kAriaDisabledAttr, AtomicString("true"));
// Assert that the AX object was affected by ARIA attributes.
auto* cache = AXObjectCache();
ASSERT_NE(nullptr, cache);
cache->UpdateAXForAllDocuments();
auto* axButton = cache->Get(button);
EXPECT_EQ(ax::mojom::Role::kCheckBox, axButton->RoleValue());
ax::mojom::NameFrom name_from;
AXObject::AXObjectVector name_objects;
EXPECT_EQ("Check", axButton->GetName(name_from, &name_objects));
EXPECT_EQ(axButton->Restriction(), kRestrictionDisabled);
// Now set the AOM properties to override.
button->accessibleNode()->setRole(AtomicString("radio"));
button->accessibleNode()->setLabel(AtomicString("Radio"));
button->accessibleNode()->setDisabled(false);
cache->UpdateAXForAllDocuments();
// Assert that AOM does not affect the AXObject.
axButton = cache->Get(button);
EXPECT_EQ(ax::mojom::Role::kCheckBox, axButton->RoleValue());
EXPECT_EQ("Check", axButton->GetName(name_from, &name_objects));
EXPECT_EQ(axButton->Restriction(), kRestrictionDisabled);
// Null the AOM properties.
button->accessibleNode()->setRole(g_null_atom);
button->accessibleNode()->setLabel(g_null_atom);
button->accessibleNode()->setDisabled(std::nullopt);
cache->UpdateAXForAllDocuments();
// The AX Object should now revert to ARIA.
axButton = cache->Get(button);
EXPECT_EQ(ax::mojom::Role::kCheckBox, axButton->RoleValue());
EXPECT_EQ("Check", axButton->GetName(name_from, &name_objects));
EXPECT_EQ(axButton->Restriction(), kRestrictionDisabled);
}
TEST_F(AccessibilityObjectModelTest, RangeProperties) {
SimRequest main_resource("https://example.com/", "text/html");
LoadURL("https://example.com/");
main_resource.Complete("<div role=slider id=slider>");
AXContext ax_context(GetDocument(), ui::kAXModeComplete);
auto* slider = GetDocument().getElementById(AtomicString("slider"));
ASSERT_NE(nullptr, slider);
slider->accessibleNode()->setValueMin(-0.5);
slider->accessibleNode()->setValueMax(0.5);
slider->accessibleNode()->setValueNow(0.1);
auto* cache = AXObjectCache();
ASSERT_NE(nullptr, cache);
cache->UpdateAXForAllDocuments();
auto* ax_slider = cache->Get(slider);
float value = 0.0f;
EXPECT_TRUE(ax_slider->MinValueForRange(&value));
EXPECT_EQ(0.0f, value);
EXPECT_TRUE(ax_slider->MaxValueForRange(&value));
EXPECT_EQ(100.0f, value);
EXPECT_TRUE(ax_slider->ValueForRange(&value));
EXPECT_EQ(50.0f, 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>");
AXContext ax_context(GetDocument(), ui::kAXModeComplete);
auto* heading = GetDocument().getElementById(AtomicString("heading"));
ASSERT_NE(nullptr, heading);
heading->accessibleNode()->setLevel(5);
auto* cache = AXObjectCache();
ASSERT_NE(nullptr, cache);
cache->UpdateAXForAllDocuments();
auto* ax_heading = cache->Get(heading);
EXPECT_EQ(2, 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>");
AXContext ax_context(GetDocument(), ui::kAXModeComplete);
auto* listitem = GetDocument().getElementById(AtomicString("listitem"));
ASSERT_NE(nullptr, listitem);
listitem->accessibleNode()->setPosInSet(9);
listitem->accessibleNode()->setSetSize(10);
auto* cache = AXObjectCache();
ASSERT_NE(nullptr, cache);
cache->UpdateAXForAllDocuments();
auto* ax_listitem = cache->Get(listitem);
EXPECT_EQ(0, ax_listitem->PosInSet());
EXPECT_EQ(0, 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");
AXContext ax_context(GetDocument(), ui::kAXModeComplete);
auto* grid = GetDocument().getElementById(AtomicString("grid"));
ASSERT_NE(nullptr, grid);
grid->accessibleNode()->setColCount(16);
grid->accessibleNode()->setRowCount(9);
auto* row = GetDocument().getElementById(AtomicString("row"));
ASSERT_NE(nullptr, row);
row->accessibleNode()->setColIndex(8);
row->accessibleNode()->setRowIndex(5);
auto* cell = GetDocument().getElementById(AtomicString("cell"));
auto* cell2 = GetDocument().getElementById(AtomicString("cell2"));
ASSERT_NE(nullptr, cell2);
cell2->accessibleNode()->setColIndex(10);
cell2->accessibleNode()->setRowIndex(7);
auto* cache = AXObjectCache();
ASSERT_NE(nullptr, cache);
cache->UpdateAXForAllDocuments();
auto* ax_grid = cache->Get(grid);
EXPECT_EQ(0, ax_grid->AriaColumnCount());
EXPECT_EQ(0, ax_grid->AriaRowCount());
auto* ax_cell = cache->Get(cell);
EXPECT_TRUE(ax_cell->IsTableCellLikeRole());
EXPECT_EQ(0U, ax_cell->AriaColumnIndex());
EXPECT_EQ(0U, ax_cell->AriaRowIndex());
auto* ax_cell2 = cache->Get(cell2);
EXPECT_TRUE(ax_cell2->IsTableCellLikeRole());
EXPECT_EQ(0U, ax_cell2->AriaColumnIndex());
EXPECT_EQ(0U, ax_cell2->AriaRowIndex());
}
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-virtualcontent=block-end
aria-activedescendant=active
aria-details=details
aria-invalid=true
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");
AXContext ax_context(GetDocument(), ui::kAXModeComplete);
auto* target = GetDocument().getElementById(AtomicString("target"));
auto* cache = AXObjectCache();
ASSERT_NE(nullptr, cache);
cache->UpdateAXForAllDocuments();
auto* ax_target = cache->Get(target);
ui::AXNodeData node_data;
ui::AXNodeData node_data2;
{
cache->UpdateAXForAllDocuments();
ScopedFreezeAXCache freeze(*cache);
ax_target->Serialize(&node_data, ui::kAXModeComplete);
ASSERT_EQ("Ctrl+K", node_data.GetStringAttribute(
ax::mojom::blink::StringAttribute::kKeyShortcuts));
ASSERT_EQ("Widget",
node_data.GetStringAttribute(
ax::mojom::blink::StringAttribute::kRoleDescription));
ASSERT_EQ("block-end",
node_data.GetStringAttribute(
ax::mojom::blink::StringAttribute::kVirtualContent));
auto* active_descendant_target =
cache->ObjectFromAXID(node_data.GetIntAttribute(
ax::mojom::blink::IntAttribute::kActivedescendantId));
ASSERT_NE(nullptr, active_descendant_target);
ASSERT_EQ(ax::mojom::Role::kListBoxOption,
active_descendant_target->RoleValue());
auto* aria_details_target =
cache->ObjectFromAXID(node_data.GetIntListAttribute(
ax::mojom::blink::IntListAttribute::kDetailsIds)[0]);
ASSERT_EQ(ax::mojom::Role::kContentInfo, aria_details_target->RoleValue());
auto* error_message_target =
cache->ObjectFromAXID(node_data.GetIntListAttribute(
ax::mojom::blink::IntListAttribute::kErrormessageIds)[0]);
ASSERT_NE(nullptr, error_message_target);
ASSERT_EQ(ax::mojom::Role::kArticle, error_message_target->RoleValue());
}
target->accessibleNode()->setKeyShortcuts(AtomicString("Ctrl+L"));
target->accessibleNode()->setRoleDescription(AtomicString("Object"));
target->accessibleNode()->setVirtualContent(AtomicString("inline-start"));
target->accessibleNode()->setActiveDescendant(
GetDocument().getElementById(AtomicString("active2"))->accessibleNode());
AccessibleNodeList* details_node_list =
MakeGarbageCollected<AccessibleNodeList>();
details_node_list->add(
GetDocument().getElementById(AtomicString("details2"))->accessibleNode());
target->accessibleNode()->setDetails(details_node_list);
AccessibleNodeList* error_message_node_list =
MakeGarbageCollected<AccessibleNodeList>();
error_message_node_list->add(
GetDocument().getElementById(AtomicString("error2"))->accessibleNode());
target->accessibleNode()->setErrorMessage(error_message_node_list);
{
cache->UpdateAXForAllDocuments();
ScopedFreezeAXCache freeze(*cache);
ax_target->Serialize(&node_data2, ui::kAXModeComplete);
ASSERT_EQ("Ctrl+K", node_data.GetStringAttribute(
ax::mojom::blink::StringAttribute::kKeyShortcuts));
ASSERT_EQ("Widget",
node_data.GetStringAttribute(
ax::mojom::blink::StringAttribute::kRoleDescription));
ASSERT_EQ(target->accessibleNode()->virtualContent(), "inline-start");
ASSERT_EQ("block-end",
node_data.GetStringAttribute(
ax::mojom::blink::StringAttribute::kVirtualContent));
auto* active_descendant_target2 =
cache->ObjectFromAXID(node_data2.GetIntAttribute(
ax::mojom::blink::IntAttribute::kActivedescendantId));
ASSERT_EQ(ax::mojom::Role::kListBoxOption,
active_descendant_target2->RoleValue());
auto* aria_details_target2 =
cache->ObjectFromAXID(node_data2.GetIntListAttribute(
ax::mojom::blink::IntListAttribute::kDetailsIds)[0]);
ASSERT_EQ(ax::mojom::Role::kContentInfo, aria_details_target2->RoleValue());
auto* error_message_target2 =
cache->ObjectFromAXID(node_data2.GetIntListAttribute(
ax::mojom::blink::IntListAttribute::kErrormessageIds)[0]);
ASSERT_NE(nullptr, error_message_target2);
ASSERT_EQ(ax::mojom::Role::kArticle, error_message_target2->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");
AXContext ax_context(GetDocument(), ui::kAXModeComplete);
auto* target = GetDocument().getElementById(AtomicString("target"));
auto* l1 = GetDocument().getElementById(AtomicString("l1"));
auto* l2 = GetDocument().getElementById(AtomicString("l2"));
auto* l3 = GetDocument().getElementById(AtomicString("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 = MakeGarbageCollected<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(2U, labeled_by.size());
}
} // namespace
} // namespace test
} // namespace blink