blob: 0d7ac6ba8ff4044ffa0f1f26b66fe6f6a7ba8f16 [file] [log] [blame]
// Copyright 2014 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 "modules/accessibility/InspectorAccessibilityAgent.h"
#include "core/HTMLNames.h"
#include "core/dom/AXObjectCache.h"
#include "core/dom/DOMNodeIds.h"
#include "core/dom/Element.h"
#include "core/inspector/IdentifiersFactory.h"
#include "core/inspector/InspectorDOMAgent.h"
#include "core/inspector/InspectorStyleSheet.h"
#include "core/page/Page.h"
#include "modules/accessibility/AXObject.h"
#include "modules/accessibility/AXObjectCacheImpl.h"
#include "modules/accessibility/InspectorTypeBuilderHelper.h"
#include <memory>
namespace blink {
using protocol::Accessibility::AXGlobalStates;
using protocol::Accessibility::AXLiveRegionAttributes;
using protocol::Accessibility::AXNode;
using protocol::Accessibility::AXNodeId;
using protocol::Accessibility::AXProperty;
using protocol::Accessibility::AXValueSource;
using protocol::Accessibility::AXValueType;
using protocol::Accessibility::AXRelatedNode;
using protocol::Accessibility::AXRelationshipAttributes;
using protocol::Accessibility::AXValue;
using protocol::Accessibility::AXWidgetAttributes;
using protocol::Accessibility::AXWidgetStates;
using namespace HTMLNames;
namespace {
void fillCoreProperties(AXObject* axObject, AXNode* nodeObject) {
// Description (secondary to the accessible name).
AXNameFrom nameFrom;
AXObject::AXObjectVector nameObjects;
axObject->name(nameFrom, &nameObjects);
AXDescriptionFrom descriptionFrom;
AXObject::AXObjectVector descriptionObjects;
String description =
axObject->description(nameFrom, descriptionFrom, &descriptionObjects);
if (!description.isEmpty())
nodeObject->setDescription(
createValue(description, AXValueTypeEnum::ComputedString));
// Value.
if (axObject->supportsRangeValue()) {
nodeObject->setValue(createValue(axObject->valueForRange()));
} else {
String stringValue = axObject->stringValue();
if (!stringValue.isEmpty())
nodeObject->setValue(createValue(stringValue));
}
}
void fillLiveRegionProperties(AXObject* axObject,
protocol::Array<AXProperty>* properties) {
if (!axObject->liveRegionRoot())
return;
properties->addItem(
createProperty(AXLiveRegionAttributesEnum::Live,
createValue(axObject->containerLiveRegionStatus(),
AXValueTypeEnum::Token)));
properties->addItem(createProperty(
AXLiveRegionAttributesEnum::Atomic,
createBooleanValue(axObject->containerLiveRegionAtomic())));
properties->addItem(
createProperty(AXLiveRegionAttributesEnum::Relevant,
createValue(axObject->containerLiveRegionRelevant(),
AXValueTypeEnum::TokenList)));
properties->addItem(
createProperty(AXLiveRegionAttributesEnum::Busy,
createBooleanValue(axObject->containerLiveRegionBusy())));
if (!axObject->isLiveRegion())
properties->addItem(
createProperty(AXLiveRegionAttributesEnum::Root,
createRelatedNodeListValue(axObject->liveRegionRoot())));
}
void fillGlobalStates(AXObject* axObject,
protocol::Array<AXProperty>* properties) {
if (!axObject->isEnabled())
properties->addItem(
createProperty(AXGlobalStatesEnum::Disabled, createBooleanValue(true)));
if (const AXObject* hiddenRoot = axObject->ariaHiddenRoot()) {
properties->addItem(
createProperty(AXGlobalStatesEnum::Hidden, createBooleanValue(true)));
properties->addItem(createProperty(AXGlobalStatesEnum::HiddenRoot,
createRelatedNodeListValue(hiddenRoot)));
}
InvalidState invalidState = axObject->getInvalidState();
switch (invalidState) {
case InvalidStateUndefined:
break;
case InvalidStateFalse:
properties->addItem(
createProperty(AXGlobalStatesEnum::Invalid,
createValue("false", AXValueTypeEnum::Token)));
break;
case InvalidStateTrue:
properties->addItem(
createProperty(AXGlobalStatesEnum::Invalid,
createValue("true", AXValueTypeEnum::Token)));
break;
case InvalidStateSpelling:
properties->addItem(
createProperty(AXGlobalStatesEnum::Invalid,
createValue("spelling", AXValueTypeEnum::Token)));
break;
case InvalidStateGrammar:
properties->addItem(
createProperty(AXGlobalStatesEnum::Invalid,
createValue("grammar", AXValueTypeEnum::Token)));
break;
default:
// TODO(aboxhall): expose invalid: <nothing> and source: aria-invalid as
// invalid value
properties->addItem(createProperty(
AXGlobalStatesEnum::Invalid,
createValue(axObject->ariaInvalidValue(), AXValueTypeEnum::String)));
break;
}
}
bool roleAllowsMultiselectable(AccessibilityRole role) {
return role == GridRole || role == ListBoxRole || role == TabListRole ||
role == TreeGridRole || role == TreeRole;
}
bool roleAllowsOrientation(AccessibilityRole role) {
return role == ScrollBarRole || role == SplitterRole || role == SliderRole;
}
bool roleAllowsReadonly(AccessibilityRole role) {
return role == GridRole || role == CellRole || role == TextFieldRole ||
role == ColumnHeaderRole || role == RowHeaderRole ||
role == TreeGridRole;
}
bool roleAllowsRequired(AccessibilityRole role) {
return role == ComboBoxRole || role == CellRole || role == ListBoxRole ||
role == RadioGroupRole || role == SpinButtonRole ||
role == TextFieldRole || role == TreeRole ||
role == ColumnHeaderRole || role == RowHeaderRole ||
role == TreeGridRole;
}
bool roleAllowsSort(AccessibilityRole role) {
return role == ColumnHeaderRole || role == RowHeaderRole;
}
bool roleAllowsChecked(AccessibilityRole role) {
return role == MenuItemCheckBoxRole || role == MenuItemRadioRole ||
role == RadioButtonRole || role == CheckBoxRole ||
role == TreeItemRole || role == ListBoxOptionRole ||
role == SwitchRole;
}
bool roleAllowsSelected(AccessibilityRole role) {
return role == CellRole || role == ListBoxOptionRole || role == RowRole ||
role == TabRole || role == ColumnHeaderRole ||
role == MenuItemRadioRole || role == RadioButtonRole ||
role == RowHeaderRole || role == TreeItemRole;
}
void fillWidgetProperties(AXObject* axObject,
protocol::Array<AXProperty>* properties) {
AccessibilityRole role = axObject->roleValue();
String autocomplete = axObject->ariaAutoComplete();
if (!autocomplete.isEmpty())
properties->addItem(
createProperty(AXWidgetAttributesEnum::Autocomplete,
createValue(autocomplete, AXValueTypeEnum::Token)));
if (axObject->hasAttribute(HTMLNames::aria_haspopupAttr)) {
bool hasPopup = axObject->ariaHasPopup();
properties->addItem(createProperty(AXWidgetAttributesEnum::Haspopup,
createBooleanValue(hasPopup)));
}
int headingLevel = axObject->headingLevel();
if (headingLevel > 0)
properties->addItem(createProperty(AXWidgetAttributesEnum::Level,
createValue(headingLevel)));
int hierarchicalLevel = axObject->hierarchicalLevel();
if (hierarchicalLevel > 0 ||
axObject->hasAttribute(HTMLNames::aria_levelAttr))
properties->addItem(createProperty(AXWidgetAttributesEnum::Level,
createValue(hierarchicalLevel)));
if (roleAllowsMultiselectable(role)) {
bool multiselectable = axObject->isMultiSelectable();
properties->addItem(createProperty(AXWidgetAttributesEnum::Multiselectable,
createBooleanValue(multiselectable)));
}
if (roleAllowsOrientation(role)) {
AccessibilityOrientation orientation = axObject->orientation();
switch (orientation) {
case AccessibilityOrientationVertical:
properties->addItem(
createProperty(AXWidgetAttributesEnum::Orientation,
createValue("vertical", AXValueTypeEnum::Token)));
break;
case AccessibilityOrientationHorizontal:
properties->addItem(
createProperty(AXWidgetAttributesEnum::Orientation,
createValue("horizontal", AXValueTypeEnum::Token)));
break;
case AccessibilityOrientationUndefined:
break;
}
}
if (role == TextFieldRole)
properties->addItem(
createProperty(AXWidgetAttributesEnum::Multiline,
createBooleanValue(axObject->isMultiline())));
if (roleAllowsReadonly(role)) {
properties->addItem(
createProperty(AXWidgetAttributesEnum::Readonly,
createBooleanValue(axObject->isReadOnly())));
}
if (roleAllowsRequired(role)) {
properties->addItem(
createProperty(AXWidgetAttributesEnum::Required,
createBooleanValue(axObject->isRequired())));
}
if (roleAllowsSort(role)) {
// TODO(aboxhall): sort
}
if (axObject->isRange()) {
properties->addItem(
createProperty(AXWidgetAttributesEnum::Valuemin,
createValue(axObject->minValueForRange())));
properties->addItem(
createProperty(AXWidgetAttributesEnum::Valuemax,
createValue(axObject->maxValueForRange())));
properties->addItem(
createProperty(AXWidgetAttributesEnum::Valuetext,
createValue(axObject->valueDescription())));
}
}
void fillWidgetStates(AXObject* axObject,
protocol::Array<AXProperty>* properties) {
AccessibilityRole role = axObject->roleValue();
if (roleAllowsChecked(role)) {
AccessibilityButtonState checked = axObject->checkboxOrRadioValue();
switch (checked) {
case ButtonStateOff:
properties->addItem(
createProperty(AXWidgetStatesEnum::Checked,
createValue("false", AXValueTypeEnum::Tristate)));
break;
case ButtonStateOn:
properties->addItem(
createProperty(AXWidgetStatesEnum::Checked,
createValue("true", AXValueTypeEnum::Tristate)));
break;
case ButtonStateMixed:
properties->addItem(
createProperty(AXWidgetStatesEnum::Checked,
createValue("mixed", AXValueTypeEnum::Tristate)));
break;
}
}
AccessibilityExpanded expanded = axObject->isExpanded();
switch (expanded) {
case ExpandedUndefined:
break;
case ExpandedCollapsed:
properties->addItem(createProperty(
AXWidgetStatesEnum::Expanded,
createBooleanValue(false, AXValueTypeEnum::BooleanOrUndefined)));
break;
case ExpandedExpanded:
properties->addItem(createProperty(
AXWidgetStatesEnum::Expanded,
createBooleanValue(true, AXValueTypeEnum::BooleanOrUndefined)));
break;
}
if (role == ToggleButtonRole) {
if (!axObject->isPressed()) {
properties->addItem(
createProperty(AXWidgetStatesEnum::Pressed,
createValue("false", AXValueTypeEnum::Tristate)));
} else {
const AtomicString& pressedAttr =
axObject->getAttribute(HTMLNames::aria_pressedAttr);
if (equalIgnoringCase(pressedAttr, "mixed"))
properties->addItem(
createProperty(AXWidgetStatesEnum::Pressed,
createValue("mixed", AXValueTypeEnum::Tristate)));
else
properties->addItem(
createProperty(AXWidgetStatesEnum::Pressed,
createValue("true", AXValueTypeEnum::Tristate)));
}
}
if (roleAllowsSelected(role)) {
properties->addItem(
createProperty(AXWidgetStatesEnum::Selected,
createBooleanValue(axObject->isSelected())));
}
}
std::unique_ptr<AXProperty> createRelatedNodeListProperty(
const String& key,
AXRelatedObjectVector& nodes) {
std::unique_ptr<AXValue> nodeListValue =
createRelatedNodeListValue(nodes, AXValueTypeEnum::NodeList);
return createProperty(key, std::move(nodeListValue));
}
std::unique_ptr<AXProperty> createRelatedNodeListProperty(
const String& key,
AXObject::AXObjectVector& nodes,
const QualifiedName& attr,
AXObject* axObject) {
std::unique_ptr<AXValue> nodeListValue = createRelatedNodeListValue(nodes);
const AtomicString& attrValue = axObject->getAttribute(attr);
nodeListValue->setValue(protocol::StringValue::create(attrValue));
return createProperty(key, std::move(nodeListValue));
}
void fillRelationships(AXObject* axObject,
protocol::Array<AXProperty>* properties) {
if (AXObject* activeDescendant = axObject->activeDescendant()) {
properties->addItem(
createProperty(AXRelationshipAttributesEnum::Activedescendant,
createRelatedNodeListValue(activeDescendant)));
}
AXObject::AXObjectVector results;
axObject->ariaFlowToElements(results);
if (!results.isEmpty())
properties->addItem(
createRelatedNodeListProperty(AXRelationshipAttributesEnum::Flowto,
results, aria_flowtoAttr, axObject));
results.clear();
axObject->ariaControlsElements(results);
if (!results.isEmpty())
properties->addItem(
createRelatedNodeListProperty(AXRelationshipAttributesEnum::Controls,
results, aria_controlsAttr, axObject));
results.clear();
axObject->ariaDescribedbyElements(results);
if (!results.isEmpty())
properties->addItem(
createRelatedNodeListProperty(AXRelationshipAttributesEnum::Describedby,
results, aria_describedbyAttr, axObject));
results.clear();
axObject->ariaOwnsElements(results);
if (!results.isEmpty())
properties->addItem(createRelatedNodeListProperty(
AXRelationshipAttributesEnum::Owns, results, aria_ownsAttr, axObject));
results.clear();
}
std::unique_ptr<AXValue> createRoleNameValue(AccessibilityRole role) {
AtomicString roleName = AXObject::roleName(role);
std::unique_ptr<AXValue> roleNameValue;
if (!roleName.isNull()) {
roleNameValue = createValue(roleName, AXValueTypeEnum::Role);
} else {
roleNameValue = createValue(AXObject::internalRoleName(role),
AXValueTypeEnum::InternalRole);
}
return roleNameValue;
}
std::unique_ptr<AXNode> buildObjectForIgnoredNode(Node* node,
const AXObject* axObject) {
AXObject::IgnoredReasons ignoredReasons;
AXID axID = 0;
std::unique_ptr<AXNode> ignoredNodeObject =
AXNode::create().setNodeId(String::number(axID)).setIgnored(true).build();
if (axObject) {
axObject->computeAccessibilityIsIgnored(&ignoredReasons);
axID = axObject->axObjectID();
AccessibilityRole role = axObject->roleValue();
ignoredNodeObject->setRole(createRoleNameValue(role));
} else if (node && !node->layoutObject()) {
ignoredReasons.append(IgnoredReason(AXNotRendered));
}
std::unique_ptr<protocol::Array<AXProperty>> ignoredReasonProperties =
protocol::Array<AXProperty>::create();
for (size_t i = 0; i < ignoredReasons.size(); i++)
ignoredReasonProperties->addItem(createProperty(ignoredReasons[i]));
ignoredNodeObject->setIgnoredReasons(std::move(ignoredReasonProperties));
return ignoredNodeObject;
}
std::unique_ptr<AXNode> buildProtocolAXObject(AXObject* axObject) {
AccessibilityRole role = axObject->roleValue();
std::unique_ptr<AXNode> nodeObject =
AXNode::create()
.setNodeId(String::number(axObject->axObjectID()))
.setIgnored(false)
.build();
nodeObject->setRole(createRoleNameValue(role));
std::unique_ptr<protocol::Array<AXProperty>> properties =
protocol::Array<AXProperty>::create();
fillLiveRegionProperties(axObject, properties.get());
fillGlobalStates(axObject, properties.get());
fillWidgetProperties(axObject, properties.get());
fillWidgetStates(axObject, properties.get());
fillRelationships(axObject, properties.get());
AXObject::NameSources nameSources;
String computedName = axObject->name(&nameSources);
if (!nameSources.isEmpty()) {
std::unique_ptr<AXValue> name =
createValue(computedName, AXValueTypeEnum::ComputedString);
if (!nameSources.isEmpty()) {
std::unique_ptr<protocol::Array<AXValueSource>> nameSourceProperties =
protocol::Array<AXValueSource>::create();
for (size_t i = 0; i < nameSources.size(); ++i) {
NameSource& nameSource = nameSources[i];
nameSourceProperties->addItem(createValueSource(nameSource));
if (nameSource.text.isNull() || nameSource.superseded)
continue;
if (!nameSource.relatedObjects.isEmpty()) {
properties->addItem(createRelatedNodeListProperty(
AXRelationshipAttributesEnum::Labelledby,
nameSource.relatedObjects));
}
}
name->setSources(std::move(nameSourceProperties));
}
nodeObject->setProperties(std::move(properties));
nodeObject->setName(std::move(name));
} else {
nodeObject->setProperties(std::move(properties));
}
fillCoreProperties(axObject, nodeObject.get());
return nodeObject;
}
} // namespace
InspectorAccessibilityAgent::InspectorAccessibilityAgent(
Page* page,
InspectorDOMAgent* domAgent)
: m_page(page), m_domAgent(domAgent) {}
void InspectorAccessibilityAgent::getAXNodeChain(
ErrorString* errorString,
int domNodeId,
bool fetchAncestors,
std::unique_ptr<protocol::Array<protocol::Accessibility::AXNode>>* nodes) {
if (!m_domAgent->enabled()) {
*errorString = "DOM agent must be enabled";
return;
}
Node* node = m_domAgent->assertNode(errorString, domNodeId);
if (!node)
return;
Document& document = node->document();
document.updateStyleAndLayoutIgnorePendingStylesheets();
DocumentLifecycle::DisallowTransitionScope disallowTransition(
document.lifecycle());
LocalFrame* localFrame = document.frame();
if (!localFrame) {
*errorString = "Frame is detached.";
return;
}
std::unique_ptr<ScopedAXObjectCache> scopedCache =
ScopedAXObjectCache::create(document);
AXObjectCacheImpl* cache = toAXObjectCacheImpl(scopedCache->get());
AXObject* axObject = cache->getOrCreate(node);
*nodes = protocol::Array<protocol::Accessibility::AXNode>::create();
if (!axObject || axObject->accessibilityIsIgnored()) {
(*nodes)->addItem(buildObjectForIgnoredNode(node, axObject));
} else {
(*nodes)->addItem(buildProtocolAXObject(axObject));
}
if (fetchAncestors && axObject) {
AXObject* parent = axObject->parentObjectUnignored();
while (parent) {
(*nodes)->addItem(buildProtocolAXObject(parent));
parent = parent->parentObjectUnignored();
}
}
}
DEFINE_TRACE(InspectorAccessibilityAgent) {
visitor->trace(m_page);
visitor->trace(m_domAgent);
InspectorBaseAgent::trace(visitor);
}
} // namespace blink