blob: 30770913131cc81227e43e0b867f8fa0b6920413 [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 "third_party/blink/renderer/modules/accessibility/inspector_accessibility_agent.h"
#include <memory>
#include "third_party/blink/renderer/core/accessibility/ax_object_cache.h"
#include "third_party/blink/renderer/core/dom/dom_node_ids.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/dom/flat_tree_traversal.h"
#include "third_party/blink/renderer/core/dom/node.h"
#include "third_party/blink/renderer/core/dom/node_list.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/html_names.h"
#include "third_party/blink/renderer/core/inspector/identifiers_factory.h"
#include "third_party/blink/renderer/core/inspector/inspected_frames.h"
#include "third_party/blink/renderer/core/inspector/inspector_dom_agent.h"
#include "third_party/blink/renderer/core/inspector/inspector_style_sheet.h"
#include "third_party/blink/renderer/modules/accessibility/ax_object.h"
#include "third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.h"
#include "third_party/blink/renderer/modules/accessibility/inspector_type_builder_helper.h"
#include "third_party/blink/renderer/platform/wtf/deque.h"
namespace blink {
using protocol::Maybe;
using protocol::Response;
using protocol::Accessibility::AXNode;
using protocol::Accessibility::AXNodeId;
using protocol::Accessibility::AXProperty;
using protocol::Accessibility::AXPropertyName;
using protocol::Accessibility::AXRelatedNode;
using protocol::Accessibility::AXValue;
using protocol::Accessibility::AXValueSource;
using protocol::Accessibility::AXValueType;
namespace AXPropertyNameEnum = protocol::Accessibility::AXPropertyNameEnum;
namespace {
static const AXID kIDForInspectedNodeWithNoAXNode = 0;
void AddHasPopupProperty(ax::mojom::HasPopup has_popup,
protocol::Array<AXProperty>& properties) {
switch (has_popup) {
case ax::mojom::HasPopup::kFalse:
break;
case ax::mojom::HasPopup::kTrue:
properties.emplace_back(
CreateProperty(AXPropertyNameEnum::HasPopup,
CreateValue("true", AXValueTypeEnum::Token)));
break;
case ax::mojom::HasPopup::kMenu:
properties.emplace_back(
CreateProperty(AXPropertyNameEnum::HasPopup,
CreateValue("menu", AXValueTypeEnum::Token)));
break;
case ax::mojom::HasPopup::kListbox:
properties.emplace_back(
CreateProperty(AXPropertyNameEnum::HasPopup,
CreateValue("listbox", AXValueTypeEnum::Token)));
break;
case ax::mojom::HasPopup::kTree:
properties.emplace_back(
CreateProperty(AXPropertyNameEnum::HasPopup,
CreateValue("tree", AXValueTypeEnum::Token)));
break;
case ax::mojom::HasPopup::kGrid:
properties.emplace_back(
CreateProperty(AXPropertyNameEnum::HasPopup,
CreateValue("grid", AXValueTypeEnum::Token)));
break;
case ax::mojom::HasPopup::kDialog:
properties.emplace_back(
CreateProperty(AXPropertyNameEnum::HasPopup,
CreateValue("dialog", AXValueTypeEnum::Token)));
break;
}
}
void FillLiveRegionProperties(AXObject& ax_object,
protocol::Array<AXProperty>& properties) {
if (!ax_object.LiveRegionRoot())
return;
properties.emplace_back(
CreateProperty(AXPropertyNameEnum::Live,
CreateValue(ax_object.ContainerLiveRegionStatus(),
AXValueTypeEnum::Token)));
properties.emplace_back(CreateProperty(
AXPropertyNameEnum::Atomic,
CreateBooleanValue(ax_object.ContainerLiveRegionAtomic())));
properties.emplace_back(
CreateProperty(AXPropertyNameEnum::Relevant,
CreateValue(ax_object.ContainerLiveRegionRelevant(),
AXValueTypeEnum::TokenList)));
if (!ax_object.IsLiveRegionRoot()) {
properties.emplace_back(CreateProperty(
AXPropertyNameEnum::Root,
CreateRelatedNodeListValue(*(ax_object.LiveRegionRoot()))));
}
}
void FillGlobalStates(AXObject& ax_object,
protocol::Array<AXProperty>& properties) {
if (ax_object.Restriction() == kRestrictionDisabled) {
properties.emplace_back(
CreateProperty(AXPropertyNameEnum::Disabled, CreateBooleanValue(true)));
}
if (const AXObject* hidden_root = ax_object.AriaHiddenRoot()) {
properties.emplace_back(
CreateProperty(AXPropertyNameEnum::Hidden, CreateBooleanValue(true)));
properties.emplace_back(
CreateProperty(AXPropertyNameEnum::HiddenRoot,
CreateRelatedNodeListValue(*hidden_root)));
}
ax::mojom::InvalidState invalid_state = ax_object.GetInvalidState();
switch (invalid_state) {
case ax::mojom::InvalidState::kNone:
break;
case ax::mojom::InvalidState::kFalse:
properties.emplace_back(
CreateProperty(AXPropertyNameEnum::Invalid,
CreateValue("false", AXValueTypeEnum::Token)));
break;
case ax::mojom::InvalidState::kTrue:
properties.emplace_back(
CreateProperty(AXPropertyNameEnum::Invalid,
CreateValue("true", AXValueTypeEnum::Token)));
break;
default:
// TODO(aboxhall): expose invalid: <nothing> and source: aria-invalid as
// invalid value
properties.emplace_back(CreateProperty(
AXPropertyNameEnum::Invalid,
CreateValue(ax_object.AriaInvalidValue(), AXValueTypeEnum::String)));
break;
}
if (ax_object.CanSetFocusAttribute()) {
properties.emplace_back(CreateProperty(
AXPropertyNameEnum::Focusable,
CreateBooleanValue(true, AXValueTypeEnum::BooleanOrUndefined)));
}
if (ax_object.IsFocused()) {
properties.emplace_back(CreateProperty(
AXPropertyNameEnum::Focused,
CreateBooleanValue(true, AXValueTypeEnum::BooleanOrUndefined)));
}
if (ax_object.IsEditable()) {
properties.emplace_back(CreateProperty(
AXPropertyNameEnum::Editable,
CreateValue(ax_object.IsRichlyEditable() ? "richtext" : "plaintext",
AXValueTypeEnum::Token)));
}
if (ax_object.CanSetValueAttribute()) {
properties.emplace_back(CreateProperty(
AXPropertyNameEnum::Settable,
CreateBooleanValue(true, AXValueTypeEnum::BooleanOrUndefined)));
}
}
bool RoleAllowsModal(ax::mojom::Role role) {
return role == ax::mojom::Role::kDialog ||
role == ax::mojom::Role::kAlertDialog;
}
bool RoleAllowsMultiselectable(ax::mojom::Role role) {
return role == ax::mojom::Role::kGrid || role == ax::mojom::Role::kListBox ||
role == ax::mojom::Role::kTabList ||
role == ax::mojom::Role::kTreeGrid || role == ax::mojom::Role::kTree;
}
bool RoleAllowsOrientation(ax::mojom::Role role) {
return role == ax::mojom::Role::kScrollBar ||
role == ax::mojom::Role::kSplitter || role == ax::mojom::Role::kSlider;
}
bool RoleAllowsReadonly(ax::mojom::Role role) {
return role == ax::mojom::Role::kGrid || role == ax::mojom::Role::kCell ||
role == ax::mojom::Role::kTextField ||
role == ax::mojom::Role::kColumnHeader ||
role == ax::mojom::Role::kRowHeader ||
role == ax::mojom::Role::kTreeGrid;
}
bool RoleAllowsRequired(ax::mojom::Role role) {
return role == ax::mojom::Role::kComboBoxGrouping ||
role == ax::mojom::Role::kComboBoxMenuButton ||
role == ax::mojom::Role::kCell || role == ax::mojom::Role::kListBox ||
role == ax::mojom::Role::kRadioGroup ||
role == ax::mojom::Role::kSpinButton ||
role == ax::mojom::Role::kTextField ||
role == ax::mojom::Role::kTextFieldWithComboBox ||
role == ax::mojom::Role::kTree ||
role == ax::mojom::Role::kColumnHeader ||
role == ax::mojom::Role::kRowHeader ||
role == ax::mojom::Role::kTreeGrid;
}
bool RoleAllowsSort(ax::mojom::Role role) {
return role == ax::mojom::Role::kColumnHeader ||
role == ax::mojom::Role::kRowHeader;
}
void FillWidgetProperties(AXObject& ax_object,
protocol::Array<AXProperty>& properties) {
ax::mojom::Role role = ax_object.RoleValue();
String autocomplete = ax_object.AutoComplete();
if (!autocomplete.IsEmpty())
properties.emplace_back(
CreateProperty(AXPropertyNameEnum::Autocomplete,
CreateValue(autocomplete, AXValueTypeEnum::Token)));
AddHasPopupProperty(ax_object.HasPopup(), properties);
int heading_level = ax_object.HeadingLevel();
if (heading_level > 0) {
properties.emplace_back(
CreateProperty(AXPropertyNameEnum::Level, CreateValue(heading_level)));
}
int hierarchical_level = ax_object.HierarchicalLevel();
if (hierarchical_level > 0 ||
ax_object.HasAttribute(html_names::kAriaLevelAttr)) {
properties.emplace_back(CreateProperty(AXPropertyNameEnum::Level,
CreateValue(hierarchical_level)));
}
if (RoleAllowsMultiselectable(role)) {
bool multiselectable = ax_object.IsMultiSelectable();
properties.emplace_back(
CreateProperty(AXPropertyNameEnum::Multiselectable,
CreateBooleanValue(multiselectable)));
}
if (RoleAllowsOrientation(role)) {
AccessibilityOrientation orientation = ax_object.Orientation();
switch (orientation) {
case kAccessibilityOrientationVertical:
properties.emplace_back(
CreateProperty(AXPropertyNameEnum::Orientation,
CreateValue("vertical", AXValueTypeEnum::Token)));
break;
case kAccessibilityOrientationHorizontal:
properties.emplace_back(
CreateProperty(AXPropertyNameEnum::Orientation,
CreateValue("horizontal", AXValueTypeEnum::Token)));
break;
case kAccessibilityOrientationUndefined:
break;
}
}
if (role == ax::mojom::Role::kTextField) {
properties.emplace_back(
CreateProperty(AXPropertyNameEnum::Multiline,
CreateBooleanValue(ax_object.IsMultiline())));
}
if (RoleAllowsReadonly(role)) {
properties.emplace_back(CreateProperty(
AXPropertyNameEnum::Readonly,
CreateBooleanValue(ax_object.Restriction() == kRestrictionReadOnly)));
}
if (RoleAllowsRequired(role)) {
properties.emplace_back(
CreateProperty(AXPropertyNameEnum::Required,
CreateBooleanValue(ax_object.IsRequired())));
}
if (RoleAllowsSort(role)) {
// TODO(aboxhall): sort
}
if (ax_object.IsRangeValueSupported()) {
float min_value;
if (ax_object.MinValueForRange(&min_value)) {
properties.emplace_back(
CreateProperty(AXPropertyNameEnum::Valuemin, CreateValue(min_value)));
}
float max_value;
if (ax_object.MaxValueForRange(&max_value)) {
properties.emplace_back(
CreateProperty(AXPropertyNameEnum::Valuemax, CreateValue(max_value)));
}
properties.emplace_back(
CreateProperty(AXPropertyNameEnum::Valuetext,
CreateValue(ax_object.ValueDescription())));
}
}
void FillWidgetStates(AXObject& ax_object,
protocol::Array<AXProperty>& properties) {
ax::mojom::Role role = ax_object.RoleValue();
const char* checked_prop_val = nullptr;
switch (ax_object.CheckedState()) {
case ax::mojom::CheckedState::kTrue:
checked_prop_val = "true";
break;
case ax::mojom::CheckedState::kMixed:
checked_prop_val = "mixed";
break;
case ax::mojom::CheckedState::kFalse:
checked_prop_val = "false";
break;
case ax::mojom::CheckedState::kNone:
break;
}
if (checked_prop_val) {
auto* const checked_prop_name = role == ax::mojom::Role::kToggleButton
? AXPropertyNameEnum::Pressed
: AXPropertyNameEnum::Checked;
properties.emplace_back(CreateProperty(
checked_prop_name,
CreateValue(checked_prop_val, AXValueTypeEnum::Tristate)));
}
AccessibilityExpanded expanded = ax_object.IsExpanded();
switch (expanded) {
case kExpandedUndefined:
break;
case kExpandedCollapsed:
properties.emplace_back(CreateProperty(
AXPropertyNameEnum::Expanded,
CreateBooleanValue(false, AXValueTypeEnum::BooleanOrUndefined)));
break;
case kExpandedExpanded:
properties.emplace_back(CreateProperty(
AXPropertyNameEnum::Expanded,
CreateBooleanValue(true, AXValueTypeEnum::BooleanOrUndefined)));
break;
}
AccessibilitySelectedState selected = ax_object.IsSelected();
switch (selected) {
case kSelectedStateUndefined:
break;
case kSelectedStateFalse:
properties.emplace_back(CreateProperty(
AXPropertyNameEnum::Selected,
CreateBooleanValue(false, AXValueTypeEnum::BooleanOrUndefined)));
break;
case kSelectedStateTrue:
properties.emplace_back(CreateProperty(
AXPropertyNameEnum::Selected,
CreateBooleanValue(true, AXValueTypeEnum::BooleanOrUndefined)));
break;
}
if (RoleAllowsModal(role)) {
properties.emplace_back(CreateProperty(
AXPropertyNameEnum::Modal, CreateBooleanValue(ax_object.IsModal())));
}
}
std::unique_ptr<AXProperty> CreateRelatedNodeListProperty(
const String& key,
AXRelatedObjectVector& nodes) {
std::unique_ptr<AXValue> node_list_value =
CreateRelatedNodeListValue(nodes, AXValueTypeEnum::NodeList);
return CreateProperty(key, std::move(node_list_value));
}
std::unique_ptr<AXProperty> CreateRelatedNodeListProperty(
const String& key,
AXObject::AXObjectVector& nodes,
const QualifiedName& attr,
AXObject& ax_object) {
std::unique_ptr<AXValue> node_list_value = CreateRelatedNodeListValue(nodes);
const AtomicString& attr_value = ax_object.GetAttribute(attr);
node_list_value->setValue(protocol::StringValue::create(attr_value));
return CreateProperty(key, std::move(node_list_value));
}
void FillRelationships(AXObject& ax_object,
protocol::Array<AXProperty>& properties) {
AXObject::AXObjectVector results;
ax_object.AriaDescribedbyElements(results);
if (!results.IsEmpty()) {
properties.emplace_back(CreateRelatedNodeListProperty(
AXPropertyNameEnum::Describedby, results,
html_names::kAriaDescribedbyAttr, ax_object));
}
results.clear();
ax_object.AriaOwnsElements(results);
if (!results.IsEmpty()) {
properties.emplace_back(
CreateRelatedNodeListProperty(AXPropertyNameEnum::Owns, results,
html_names::kAriaOwnsAttr, ax_object));
}
results.clear();
}
void GetObjectsFromAXIDs(const AXObjectCacheImpl& cache,
const std::vector<int32_t>& ax_ids,
AXObject::AXObjectVector* ax_objects) {
for (const auto& ax_id : ax_ids) {
AXObject* ax_object = cache.ObjectFromAXID(ax_id);
if (!ax_object)
continue;
ax_objects->push_back(ax_object);
}
}
void FillSparseAttributes(AXObject& ax_object,
protocol::Array<AXProperty>& properties) {
ui::AXNodeData node_data;
ax_object.Serialize(&node_data, ui::kAXModeComplete);
if (node_data.HasBoolAttribute(ax::mojom::blink::BoolAttribute::kBusy)) {
const auto is_busy =
node_data.GetBoolAttribute(ax::mojom::blink::BoolAttribute::kBusy);
properties.emplace_back(
CreateProperty(AXPropertyNameEnum::Busy,
CreateValue(is_busy, AXValueTypeEnum::Boolean)));
}
if (node_data.HasStringAttribute(
ax::mojom::blink::StringAttribute::kKeyShortcuts)) {
const auto key_shortcuts = node_data.GetStringAttribute(
ax::mojom::blink::StringAttribute::kKeyShortcuts);
properties.emplace_back(
CreateProperty(AXPropertyNameEnum::Keyshortcuts,
CreateValue(WTF::String(key_shortcuts.c_str()),
AXValueTypeEnum::String)));
}
if (node_data.HasStringAttribute(
ax::mojom::blink::StringAttribute::kRoleDescription)) {
const auto role_description = node_data.GetStringAttribute(
ax::mojom::blink::StringAttribute::kRoleDescription);
properties.emplace_back(
CreateProperty(AXPropertyNameEnum::Roledescription,
CreateValue(WTF::String(role_description.c_str()),
AXValueTypeEnum::String)));
}
if (node_data.HasIntAttribute(
ax::mojom::blink::IntAttribute::kActivedescendantId)) {
AXObject* target =
ax_object.AXObjectCache().ObjectFromAXID(node_data.GetIntAttribute(
ax::mojom::blink::IntAttribute::kActivedescendantId));
properties.emplace_back(
CreateProperty(AXPropertyNameEnum::Activedescendant,
CreateRelatedNodeListValue(*target)));
}
if (node_data.HasIntAttribute(
ax::mojom::blink::IntAttribute::kErrormessageId)) {
AXObject* target =
ax_object.AXObjectCache().ObjectFromAXID(node_data.GetIntAttribute(
ax::mojom::blink::IntAttribute::kErrormessageId));
properties.emplace_back(CreateProperty(
AXPropertyNameEnum::Errormessage, CreateRelatedNodeListValue(*target)));
}
if (node_data.HasIntListAttribute(
ax::mojom::blink::IntListAttribute::kControlsIds)) {
const auto ax_ids = node_data.GetIntListAttribute(
ax::mojom::blink::IntListAttribute::kControlsIds);
AXObject::AXObjectVector ax_objects;
GetObjectsFromAXIDs(ax_object.AXObjectCache(), ax_ids, &ax_objects);
properties.emplace_back(CreateRelatedNodeListProperty(
AXPropertyNameEnum::Controls, ax_objects, html_names::kAriaControlsAttr,
ax_object));
}
if (node_data.HasIntListAttribute(
ax::mojom::blink::IntListAttribute::kDetailsIds)) {
const auto ax_ids = node_data.GetIntListAttribute(
ax::mojom::blink::IntListAttribute::kDetailsIds);
AXObject::AXObjectVector ax_objects;
GetObjectsFromAXIDs(ax_object.AXObjectCache(), ax_ids, &ax_objects);
properties.emplace_back(
CreateRelatedNodeListProperty(AXPropertyNameEnum::Details, ax_objects,
html_names::kAriaDetailsAttr, ax_object));
}
if (node_data.HasIntListAttribute(
ax::mojom::blink::IntListAttribute::kFlowtoIds)) {
const auto ax_ids = node_data.GetIntListAttribute(
ax::mojom::blink::IntListAttribute::kFlowtoIds);
AXObject::AXObjectVector ax_objects;
GetObjectsFromAXIDs(ax_object.AXObjectCache(), ax_ids, &ax_objects);
properties.emplace_back(
CreateRelatedNodeListProperty(AXPropertyNameEnum::Flowto, ax_objects,
html_names::kAriaFlowtoAttr, ax_object));
}
return;
}
std::unique_ptr<AXValue> CreateRoleNameValue(ax::mojom::Role role) {
AtomicString role_name = AXObject::RoleName(role);
std::unique_ptr<AXValue> role_name_value;
if (!role_name.IsNull()) {
role_name_value = CreateValue(role_name, AXValueTypeEnum::Role);
} else {
role_name_value = CreateValue(AXObject::InternalRoleName(role),
AXValueTypeEnum::InternalRole);
}
return role_name_value;
}
} // namespace
using EnabledAgentsMultimap =
HeapHashMap<WeakMember<LocalFrame>,
Member<HeapHashSet<Member<InspectorAccessibilityAgent>>>>;
EnabledAgentsMultimap& EnabledAgents() {
DEFINE_STATIC_LOCAL(Persistent<EnabledAgentsMultimap>, enabled_agents,
(MakeGarbageCollected<EnabledAgentsMultimap>()));
return *enabled_agents;
}
InspectorAccessibilityAgent::InspectorAccessibilityAgent(
InspectedFrames* inspected_frames,
InspectorDOMAgent* dom_agent)
: inspected_frames_(inspected_frames),
dom_agent_(dom_agent),
enabled_(&agent_state_, /*default_value=*/false) {}
Response InspectorAccessibilityAgent::getPartialAXTree(
Maybe<int> dom_node_id,
Maybe<int> backend_node_id,
Maybe<String> object_id,
Maybe<bool> fetch_relatives,
std::unique_ptr<protocol::Array<AXNode>>* nodes) {
Node* dom_node = nullptr;
Response response =
dom_agent_->AssertNode(dom_node_id, backend_node_id, object_id, dom_node);
if (!response.IsSuccess())
return response;
Document& document = dom_node->GetDocument();
document.UpdateStyleAndLayout(DocumentUpdateReason::kInspector);
DocumentLifecycle::DisallowTransitionScope disallow_transition(
document.Lifecycle());
LocalFrame* local_frame = document.GetFrame();
if (!local_frame)
return Response::ServerError("Frame is detached.");
AXContext ax_context(document);
auto& cache = To<AXObjectCacheImpl>(ax_context.GetAXObjectCache());
AXObject* inspected_ax_object = cache.GetOrCreate(dom_node);
*nodes = std::make_unique<protocol::Array<protocol::Accessibility::AXNode>>();
if (!inspected_ax_object || inspected_ax_object->AccessibilityIsIgnored()) {
(*nodes)->emplace_back(BuildObjectForIgnoredNode(
dom_node, inspected_ax_object, fetch_relatives.fromMaybe(true), *nodes,
cache));
return Response::Success();
} else {
(*nodes)->emplace_back(
BuildProtocolAXObject(*inspected_ax_object, inspected_ax_object,
fetch_relatives.fromMaybe(true), *nodes, cache));
}
if (!inspected_ax_object)
return Response::Success();
AXObject* parent = inspected_ax_object->ParentObjectUnignored();
if (!parent)
return Response::Success();
if (fetch_relatives.fromMaybe(true))
AddAncestors(*parent, inspected_ax_object, *nodes, cache);
return Response::Success();
}
void InspectorAccessibilityAgent::AddAncestors(
AXObject& first_ancestor,
AXObject* inspected_ax_object,
std::unique_ptr<protocol::Array<AXNode>>& nodes,
AXObjectCacheImpl& cache) const {
AXObject* ancestor = &first_ancestor;
AXObject* child = inspected_ax_object;
while (ancestor) {
std::unique_ptr<AXNode> parent_node_object = BuildProtocolAXObject(
*ancestor, inspected_ax_object, true, nodes, cache);
auto child_ids = std::make_unique<protocol::Array<AXNodeId>>();
if (child)
child_ids->emplace_back(String::Number(child->AXObjectID()));
else
child_ids->emplace_back(String::Number(kIDForInspectedNodeWithNoAXNode));
parent_node_object->setChildIds(std::move(child_ids));
nodes->emplace_back(std::move(parent_node_object));
child = ancestor;
ancestor = ancestor->ParentObjectUnignored();
}
}
std::unique_ptr<AXNode> InspectorAccessibilityAgent::BuildObjectForIgnoredNode(
Node* dom_node,
AXObject* ax_object,
bool fetch_relatives,
std::unique_ptr<protocol::Array<AXNode>>& nodes,
AXObjectCacheImpl& cache) const {
AXObject::IgnoredReasons ignored_reasons;
AXID ax_id = kIDForInspectedNodeWithNoAXNode;
if (ax_object && ax_object->IsAXLayoutObject())
ax_id = ax_object->AXObjectID();
std::unique_ptr<AXNode> ignored_node_object =
AXNode::create()
.setNodeId(String::Number(ax_id))
.setIgnored(true)
.build();
ax::mojom::Role role = ax::mojom::Role::kIgnored;
ignored_node_object->setRole(CreateRoleNameValue(role));
if (ax_object && ax_object->IsAXLayoutObject()) {
ax_object->ComputeAccessibilityIsIgnored(&ignored_reasons);
AXObject* parent_object = ax_object->ParentObjectUnignored();
if (parent_object && fetch_relatives)
AddAncestors(*parent_object, ax_object, nodes, cache);
} else if (dom_node && !dom_node->GetLayoutObject()) {
if (fetch_relatives) {
PopulateDOMNodeAncestors(*dom_node, *(ignored_node_object.get()), nodes,
cache);
}
ignored_reasons.emplace_back(IgnoredReason(kAXNotRendered));
}
if (dom_node) {
ignored_node_object->setBackendDOMNodeId(
IdentifiersFactory::IntIdForNode(dom_node));
}
auto ignored_reason_properties =
std::make_unique<protocol::Array<AXProperty>>();
for (IgnoredReason& reason : ignored_reasons)
ignored_reason_properties->emplace_back(CreateProperty(reason));
ignored_node_object->setIgnoredReasons(std::move(ignored_reason_properties));
return ignored_node_object;
}
void InspectorAccessibilityAgent::PopulateDOMNodeAncestors(
Node& inspected_dom_node,
AXNode& node_object,
std::unique_ptr<protocol::Array<AXNode>>& nodes,
AXObjectCacheImpl& cache) const {
// Walk up parents until an AXObject can be found.
auto* shadow_root = DynamicTo<ShadowRoot>(inspected_dom_node);
Node* parent_node = shadow_root
? &shadow_root->host()
: FlatTreeTraversal::Parent(inspected_dom_node);
AXObject* parent_ax_object = cache.GetOrCreate(parent_node);
while (parent_node && !parent_ax_object) {
shadow_root = DynamicTo<ShadowRoot>(parent_node);
parent_node = shadow_root ? &shadow_root->host()
: FlatTreeTraversal::Parent(*parent_node);
parent_ax_object = cache.GetOrCreate(parent_node);
}
if (!parent_ax_object)
return;
if (parent_ax_object->AccessibilityIsIgnored())
parent_ax_object = parent_ax_object->ParentObjectUnignored();
if (!parent_ax_object)
return;
// Populate parent and ancestors.
AddAncestors(*parent_ax_object, nullptr, nodes, cache);
}
std::unique_ptr<AXNode> InspectorAccessibilityAgent::BuildProtocolAXObject(
AXObject& ax_object,
AXObject* inspected_ax_object,
bool fetch_relatives,
std::unique_ptr<protocol::Array<AXNode>>& nodes,
AXObjectCacheImpl& cache) const {
ax::mojom::Role role = ax_object.RoleValue();
std::unique_ptr<AXNode> node_object =
AXNode::create()
.setNodeId(String::Number(ax_object.AXObjectID()))
.setIgnored(false)
.build();
node_object->setRole(CreateRoleNameValue(role));
auto properties = std::make_unique<protocol::Array<AXProperty>>();
FillLiveRegionProperties(ax_object, *(properties.get()));
FillGlobalStates(ax_object, *(properties.get()));
FillWidgetProperties(ax_object, *(properties.get()));
FillWidgetStates(ax_object, *(properties.get()));
FillRelationships(ax_object, *(properties.get()));
FillSparseAttributes(ax_object, *(properties.get()));
AXObject::NameSources name_sources;
String computed_name = ax_object.GetName(&name_sources);
if (!name_sources.IsEmpty()) {
std::unique_ptr<AXValue> name =
CreateValue(computed_name, AXValueTypeEnum::ComputedString);
if (!name_sources.IsEmpty()) {
auto name_source_properties =
std::make_unique<protocol::Array<AXValueSource>>();
for (NameSource& name_source : name_sources) {
name_source_properties->emplace_back(CreateValueSource(name_source));
if (name_source.text.IsNull() || name_source.superseded)
continue;
if (!name_source.related_objects.IsEmpty()) {
properties->emplace_back(CreateRelatedNodeListProperty(
AXPropertyNameEnum::Labelledby, name_source.related_objects));
}
}
name->setSources(std::move(name_source_properties));
}
node_object->setProperties(std::move(properties));
node_object->setName(std::move(name));
} else {
node_object->setProperties(std::move(properties));
}
FillCoreProperties(ax_object, inspected_ax_object, fetch_relatives,
*(node_object.get()), nodes, cache);
return node_object;
}
Response InspectorAccessibilityAgent::getFullAXTree(
protocol::Maybe<int> max_depth,
std::unique_ptr<protocol::Array<AXNode>>* nodes) {
Document* document = inspected_frames_->Root()->GetDocument();
if (!document)
return Response::ServerError("No document.");
if (document->View()->NeedsLayout() || document->NeedsLayoutTreeUpdate())
document->UpdateStyleAndLayout(DocumentUpdateReason::kInspector);
*nodes = WalkAXNodesToDepth(document, max_depth.fromMaybe(-1));
return Response::Success();
}
std::unique_ptr<protocol::Array<AXNode>>
InspectorAccessibilityAgent::WalkAXNodesToDepth(Document* document,
int max_depth) {
std::unique_ptr<protocol::Array<AXNode>> nodes =
std::make_unique<protocol::Array<protocol::Accessibility::AXNode>>();
AXContext ax_context(*document);
auto& cache = To<AXObjectCacheImpl>(ax_context.GetAXObjectCache());
Deque<std::pair<AXID, int>> id_depths;
id_depths.emplace_back(cache.Root()->AXObjectID(), 0);
while (!id_depths.empty()) {
std::pair<AXID, int> id_depth = id_depths.front();
id_depths.pop_front();
AXObject* ax_object = cache.ObjectFromAXID(id_depth.first);
std::unique_ptr<AXNode> node =
BuildProtocolAXObject(*ax_object, nullptr, false, nodes, cache);
auto child_ids = std::make_unique<protocol::Array<AXNodeId>>();
const AXObject::AXObjectVector& children = ax_object->UnignoredChildren();
for (auto& child_ax_object : children) {
child_ids->emplace_back(String::Number(child_ax_object->AXObjectID()));
int depth = id_depth.second;
if (max_depth == -1 || depth < max_depth)
id_depths.emplace_back(child_ax_object->AXObjectID(), depth + 1);
}
node->setChildIds(std::move(child_ids));
nodes->emplace_back(std::move(node));
}
return nodes;
}
protocol::Response InspectorAccessibilityAgent::getChildAXNodes(
const String& in_id,
std::unique_ptr<protocol::Array<protocol::Accessibility::AXNode>>*
out_nodes) {
if (!enabled_.Get())
return Response::ServerError("Accessibility has not been enabled.");
// FIXME(aboxhall): specify a document to this and getRootAXNode()
Document* document = inspected_frames_->Root()->GetDocument();
if (!document)
return Response::ServerError("No document.");
if (document->View()->NeedsLayout() || document->NeedsLayoutTreeUpdate())
document->UpdateStyleAndLayout(DocumentUpdateReason::kInspector);
// Since we called enable(), this should exist.
AXObjectCacheImpl* cache =
To<AXObjectCacheImpl>(document->ExistingAXObjectCache());
if (!cache)
return Response::ServerError("No AXObjectCache.");
AXID ax_id = in_id.ToUInt();
AXObject* ax_object = cache->ObjectFromAXID(ax_id);
if (!ax_object)
return Response::InvalidParams("Invalid ID");
*out_nodes =
std::make_unique<protocol::Array<protocol::Accessibility::AXNode>>();
const AXObject::AXObjectVector& children = ax_object->UnignoredChildren();
for (auto& child_ax_object : children) {
std::unique_ptr<AXNode> child_node = BuildProtocolAXObject(
*child_ax_object, nullptr, false, *out_nodes, *cache);
auto grandchild_ids = std::make_unique<protocol::Array<AXNodeId>>();
const AXObject::AXObjectVector& grandchildren =
child_ax_object->UnignoredChildren();
for (AXObject* grandchild : grandchildren)
grandchild_ids->emplace_back(String::Number(grandchild->AXObjectID()));
child_node->setChildIds(std::move(grandchild_ids));
(*out_nodes)->emplace_back(std::move(child_node));
}
return Response::Success();
}
void InspectorAccessibilityAgent::FillCoreProperties(
AXObject& ax_object,
AXObject* inspected_ax_object,
bool fetch_relatives,
AXNode& node_object,
std::unique_ptr<protocol::Array<AXNode>>& nodes,
AXObjectCacheImpl& cache) const {
ax::mojom::NameFrom name_from;
AXObject::AXObjectVector name_objects;
ax_object.GetName(name_from, &name_objects);
ax::mojom::DescriptionFrom description_from;
AXObject::AXObjectVector description_objects;
String description =
ax_object.Description(name_from, description_from, &description_objects);
if (!description.IsEmpty()) {
node_object.setDescription(
CreateValue(description, AXValueTypeEnum::ComputedString));
}
// Value.
if (ax_object.IsRangeValueSupported()) {
float value;
if (ax_object.ValueForRange(&value))
node_object.setValue(CreateValue(value));
} else {
String string_value = ax_object.StringValue();
if (!string_value.IsEmpty())
node_object.setValue(CreateValue(string_value));
}
if (fetch_relatives)
PopulateRelatives(ax_object, inspected_ax_object, node_object, nodes,
cache);
Node* node = ax_object.GetNode();
if (node)
node_object.setBackendDOMNodeId(IdentifiersFactory::IntIdForNode(node));
}
void InspectorAccessibilityAgent::PopulateRelatives(
AXObject& ax_object,
AXObject* inspected_ax_object,
AXNode& node_object,
std::unique_ptr<protocol::Array<AXNode>>& nodes,
AXObjectCacheImpl& cache) const {
AXObject* parent_object = ax_object.ParentObject();
if (parent_object && parent_object != inspected_ax_object) {
// Use unignored parent unless parent is inspected ignored object.
parent_object = ax_object.ParentObjectUnignored();
}
auto child_ids = std::make_unique<protocol::Array<AXNodeId>>();
if (!ax_object.AccessibilityIsIgnored())
AddChildren(ax_object, inspected_ax_object, child_ids, nodes, cache);
node_object.setChildIds(std::move(child_ids));
}
void InspectorAccessibilityAgent::AddChildren(
AXObject& ax_object,
AXObject* inspected_ax_object,
std::unique_ptr<protocol::Array<AXNodeId>>& child_ids,
std::unique_ptr<protocol::Array<AXNode>>& nodes,
AXObjectCacheImpl& cache) const {
if (inspected_ax_object && inspected_ax_object->AccessibilityIsIgnored() &&
&ax_object == inspected_ax_object->ParentObjectUnignored()) {
child_ids->emplace_back(String::Number(inspected_ax_object->AXObjectID()));
return;
}
const AXObject::AXObjectVector& children = ax_object.UnignoredChildren();
for (unsigned i = 0; i < children.size(); i++) {
AXObject& child_ax_object = *children[i].Get();
child_ids->emplace_back(String::Number(child_ax_object.AXObjectID()));
if (&child_ax_object == inspected_ax_object)
continue;
if (&ax_object != inspected_ax_object) {
if (!inspected_ax_object)
continue;
if (&ax_object != inspected_ax_object->ParentObjectUnignored() &&
ax_object.GetNode())
continue;
}
// Only add children of inspected node (or un-inspectable children of
// inspected node) to returned nodes.
std::unique_ptr<AXNode> child_node = BuildProtocolAXObject(
child_ax_object, inspected_ax_object, true, nodes, cache);
nodes->emplace_back(std::move(child_node));
}
}
namespace {
void setNameAndRole(const AXObject& ax_object, std::unique_ptr<AXNode>& node) {
ax::mojom::blink::Role role = ax_object.RoleValue();
node->setRole(CreateRoleNameValue(role));
AXObject::NameSources name_sources;
String computed_name = ax_object.GetName(&name_sources);
std::unique_ptr<AXValue> name =
CreateValue(computed_name, AXValueTypeEnum::ComputedString);
node->setName(std::move(name));
}
} // namespace
Response InspectorAccessibilityAgent::queryAXTree(
Maybe<int> dom_node_id,
Maybe<int> backend_node_id,
Maybe<String> object_id,
Maybe<String> accessible_name,
Maybe<String> role,
std::unique_ptr<protocol::Array<AXNode>>* nodes) {
Node* root_dom_node = nullptr;
Response response = dom_agent_->AssertNode(dom_node_id, backend_node_id,
object_id, root_dom_node);
if (!response.IsSuccess())
return response;
Document& document = root_dom_node->GetDocument();
document.UpdateStyleAndLayout(DocumentUpdateReason::kInspector);
DocumentLifecycle::DisallowTransitionScope disallow_transition(
document.Lifecycle());
AXContext ax_context(document);
*nodes = std::make_unique<protocol::Array<protocol::Accessibility::AXNode>>();
auto& cache = To<AXObjectCacheImpl>(ax_context.GetAXObjectCache());
AXObject* root_ax_node = cache.GetOrCreate(root_dom_node);
auto sought_role = ax::mojom::blink::Role::kUnknown;
if (role.isJust())
sought_role = AXObject::AriaRoleToWebCoreRole(role.fromJust());
const String sought_name = accessible_name.fromMaybe("");
HeapVector<Member<AXObject>> reachable;
reachable.push_back(root_ax_node);
while (!reachable.IsEmpty()) {
AXObject* ax_object = reachable.back();
reachable.pop_back();
const AXObject::AXObjectVector& children = ax_object->UnignoredChildren();
reachable.AppendRange(children.rbegin(), children.rend());
// if querying by name: skip if name of current object does not match.
if (accessible_name.isJust() && sought_name != ax_object->ComputedName())
continue;
// if querying by role: skip if role of current object does not match.
if (role.isJust() && sought_role != ax_object->RoleValue())
continue;
// both name and role are OK, so we can add current object to the result.
if (ax_object->AccessibilityIsIgnored()) {
Node* dom_node = ax_object->GetNode();
std::unique_ptr<AXNode> protocol_node =
BuildObjectForIgnoredNode(dom_node, ax_object, false, *nodes, cache);
setNameAndRole(*ax_object, protocol_node);
(*nodes)->push_back(std::move(protocol_node));
} else {
(*nodes)->push_back(
BuildProtocolAXObject(*ax_object, nullptr, false, *nodes, cache));
}
}
return Response::Success();
}
void InspectorAccessibilityAgent::EnableAndReset() {
enabled_.Set(true);
LocalFrame* frame = inspected_frames_->Root();
if (!EnabledAgents().Contains(frame)) {
EnabledAgents().Set(
frame, MakeGarbageCollected<
HeapHashSet<Member<InspectorAccessibilityAgent>>>());
}
EnabledAgents().find(frame)->value->insert(this);
CreateAXContext();
}
protocol::Response InspectorAccessibilityAgent::enable() {
if (!enabled_.Get())
EnableAndReset();
return Response::Success();
}
protocol::Response InspectorAccessibilityAgent::disable() {
if (!enabled_.Get())
return Response::Success();
enabled_.Set(false);
context_ = nullptr;
LocalFrame* frame = inspected_frames_->Root();
DCHECK(EnabledAgents().Contains(frame));
auto it = EnabledAgents().find(frame);
it->value->erase(this);
if (it->value->IsEmpty())
EnabledAgents().erase(frame);
return Response::Success();
}
void InspectorAccessibilityAgent::Restore() {
if (enabled_.Get())
EnableAndReset();
}
void InspectorAccessibilityAgent::ProvideTo(LocalFrame* frame) {
if (!EnabledAgents().Contains(frame))
return;
for (InspectorAccessibilityAgent* agent : *EnabledAgents().find(frame)->value)
agent->CreateAXContext();
}
void InspectorAccessibilityAgent::CreateAXContext() {
Document* document = inspected_frames_->Root()->GetDocument();
if (document)
context_ = std::make_unique<AXContext>(*document);
}
void InspectorAccessibilityAgent::Trace(Visitor* visitor) const {
visitor->Trace(inspected_frames_);
visitor->Trace(dom_agent_);
InspectorBaseAgent::Trace(visitor);
}
} // namespace blink