Commit 10055e9d authored by Alexander Surkov's avatar Alexander Surkov Committed by Commit Bot

DumpAccTree mac testing: refactor tree formatter logic to make it

suitable for nested attribute calls

Bug: 1100991
AX-Relnotes: n/a
Change-Id: Ib0c4512242bad94abf8a0231e129951172e6b584
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2274545
Commit-Queue: Alexander Surkov <asurkov@igalia.com>
Reviewed-by: default avatarDominic Mazzoni <dmazzoni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#796577}
parent c4218521
...@@ -46,7 +46,10 @@ PropertyNode PropertyNode::FromPropertyFilter( ...@@ -46,7 +46,10 @@ PropertyNode PropertyNode::FromPropertyFilter(
Parse(&root, filter.property_str.begin(), filter.property_str.end()); Parse(&root, filter.property_str.begin(), filter.property_str.end());
PropertyNode* node = &root.parameters[0]; PropertyNode* node = &root.parameters[0];
node->original_property = filter.property_str;
// Expel a trailing wildcard if any.
node->original_property =
filter.property_str.substr(0, filter.property_str.find_last_of('*'));
// Line indexes filter: filter_str expected format is // Line indexes filter: filter_str expected format is
// :line_num_1, ... :line_num_N, a comma separated list of line indexes // :line_num_1, ... :line_num_N, a comma separated list of line indexes
...@@ -83,6 +86,13 @@ PropertyNode::operator bool() const { ...@@ -83,6 +86,13 @@ PropertyNode::operator bool() const {
return !name_or_value.empty(); return !name_or_value.empty();
} }
bool PropertyNode::IsMatching(const base::string16& pattern) const {
// Looking for exact property match. Expel a trailing whildcard from
// the property filter to handle filters like AXRole*.
return name_or_value.compare(0, name_or_value.find_last_of('*'), pattern) ==
0;
}
bool PropertyNode::IsArray() const { bool PropertyNode::IsArray() const {
return name_or_value == base::ASCIIToUTF16("[]"); return name_or_value == base::ASCIIToUTF16("[]");
} }
...@@ -439,14 +449,14 @@ AccessibilityTreeFormatterBase::GetVersionSpecificExpectedFileSuffix() { ...@@ -439,14 +449,14 @@ AccessibilityTreeFormatterBase::GetVersionSpecificExpectedFileSuffix() {
return FILE_PATH_LITERAL(""); return FILE_PATH_LITERAL("");
} }
PropertyNode AccessibilityTreeFormatterBase::GetMatchingPropertyNode( std::vector<PropertyNode>
const base::string16& line_index, AccessibilityTreeFormatterBase::PropertyFilterNodesFor(
const base::string16& property_name) { const base::string16& line_index) const {
// Find the first allow-filter matching the line index and the property name. std::vector<PropertyNode> list;
for (const auto& filter : property_filters_) { for (const auto& filter : property_filters_) {
PropertyNode property_node = PropertyNode::FromPropertyFilter(filter); PropertyNode property_node = PropertyNode::FromPropertyFilter(filter);
// Skip if the line index filter doesn't matched (if specified). // Filter out if doesn't match line index (if specified).
if (!property_node.line_indexes.empty() && if (!property_node.line_indexes.empty() &&
std::find(property_node.line_indexes.begin(), std::find(property_node.line_indexes.begin(),
property_node.line_indexes.end(), property_node.line_indexes.end(),
...@@ -454,23 +464,28 @@ PropertyNode AccessibilityTreeFormatterBase::GetMatchingPropertyNode( ...@@ -454,23 +464,28 @@ PropertyNode AccessibilityTreeFormatterBase::GetMatchingPropertyNode(
continue; continue;
} }
// The filter should be either an exact property match or a wildcard switch (filter.type) {
// matching to support filter collections like AXRole* which matches case PropertyFilter::ALLOW_EMPTY:
// AXRoleDescription. case PropertyFilter::ALLOW:
if (property_name == property_node.name_or_value || list.push_back(std::move(property_node));
base::MatchPattern(property_name, property_node.name_or_value)) { break;
switch (filter.type) { case PropertyFilter::DENY:
case PropertyFilter::ALLOW_EMPTY: break;
case PropertyFilter::ALLOW: default:
return property_node; break;
case PropertyFilter::DENY: }
break; }
default: return list;
break; }
}
bool AccessibilityTreeFormatterBase::HasMatchAllPropertyFilter() const {
for (const auto& filter : property_filters_) {
if (filter.type == PropertyFilter::ALLOW &&
filter.match_str == base::ASCIIToUTF16("*")) {
return true;
} }
} }
return PropertyNode(); return false;
} }
bool AccessibilityTreeFormatterBase::MatchesPropertyFilters( bool AccessibilityTreeFormatterBase::MatchesPropertyFilters(
......
...@@ -65,6 +65,8 @@ class CONTENT_EXPORT PropertyNode final { ...@@ -65,6 +65,8 @@ class CONTENT_EXPORT PropertyNode final {
// be called for. // be called for.
std::vector<base::string16> line_indexes; std::vector<base::string16> line_indexes;
bool IsMatching(const base::string16& pattern) const;
// Argument conversion methods. // Argument conversion methods.
bool IsArray() const; bool IsArray() const;
bool IsDict() const; bool IsDict() const;
...@@ -151,11 +153,13 @@ class CONTENT_EXPORT AccessibilityTreeFormatterBase ...@@ -151,11 +153,13 @@ class CONTENT_EXPORT AccessibilityTreeFormatterBase
// Overridden by platform subclasses. // Overridden by platform subclasses.
// //
// Returns a property node struct built for a matching property filter, // Returns property nodes complying to the line index filter for all
// which includes a property name and invocation parameters if any. // allow/allow_empty property filters.
// If no matching property filter, then empty property node is returned. std::vector<PropertyNode> PropertyFilterNodesFor(
PropertyNode GetMatchingPropertyNode(const base::string16& line_index, const base::string16& line_index) const;
const base::string16& property_name);
// Return true if match-all filter is present.
bool HasMatchAllPropertyFilter() const;
// Process accessibility tree with filters for output. // Process accessibility tree with filters for output.
// Given a dictionary that contains a platform-specific dictionary // Given a dictionary that contains a platform-specific dictionary
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
#ifndef CONTENT_BROWSER_ACCESSIBILITY_ACCESSIBILITY_TREE_FORMATTER_UTILS_MAC_H_ #ifndef CONTENT_BROWSER_ACCESSIBILITY_ACCESSIBILITY_TREE_FORMATTER_UTILS_MAC_H_
#define CONTENT_BROWSER_ACCESSIBILITY_ACCESSIBILITY_TREE_FORMATTER_UTILS_MAC_H_ #define CONTENT_BROWSER_ACCESSIBILITY_ACCESSIBILITY_TREE_FORMATTER_UTILS_MAC_H_
#include "content/browser/accessibility/accessibility_tree_formatter_base.h"
#include "content/browser/accessibility/browser_accessibility_cocoa.h" #include "content/browser/accessibility/browser_accessibility_cocoa.h"
namespace content { namespace content {
...@@ -28,6 +29,68 @@ class LineIndexesMap { ...@@ -28,6 +29,68 @@ class LineIndexesMap {
std::map<const gfx::NativeViewAccessible, std::string> map; std::map<const gfx::NativeViewAccessible, std::string> map;
}; };
// Implements stateful id values. Can be either id or be in
// error or not applciable state. Similar to base::Optional, but tri-state
// allowing nullable values.
class OptionalNSObject final {
public:
enum { ID, ERROR, NOT_APPLICABLE };
static OptionalNSObject Error() { return OptionalNSObject(ERROR); }
static OptionalNSObject NotApplicable() {
return OptionalNSObject(NOT_APPLICABLE);
}
static OptionalNSObject NotNilOrError(id other_value) {
return OptionalNSObject(other_value, other_value ? ID : ERROR);
}
static OptionalNSObject NotNullOrNotApplicable(id other_value) {
return OptionalNSObject(other_value, other_value ? ID : NOT_APPLICABLE);
}
OptionalNSObject(int flag) : value(nil), flag(flag) {}
OptionalNSObject(id value, int flag = ID) : value(value), flag(flag) {}
bool IsNotApplicable() const { return flag == NOT_APPLICABLE; }
bool IsError() const { return flag == ERROR; }
bool IsNotNil() const { return value != nil; }
constexpr const id& operator*() const& { return value; }
private:
id value;
int flag;
};
// Invokes attributes matching the given property filter.
class AttributeInvoker final {
public:
AttributeInvoker(const BrowserAccessibilityCocoa* cocoa_node,
const LineIndexesMap& line_indexes_map);
// Invokes an attribute matching to a property filter.
OptionalNSObject Invoke(const PropertyNode& property_node) const;
private:
// Returns a parameterized attribute parameter by a property node.
OptionalNSObject ParamByPropertyNode(const PropertyNode&) const;
NSNumber* PropertyNodeToInt(const PropertyNode&) const;
NSArray* PropertyNodeToIntArray(const PropertyNode&) const;
NSValue* PropertyNodeToRange(const PropertyNode&) const;
gfx::NativeViewAccessible PropertyNodeToUIElement(const PropertyNode&) const;
id DictNodeToTextMarker(const PropertyNode&) const;
id PropertyNodeToTextMarker(const PropertyNode&) const;
id PropertyNodeToTextMarkerRange(const PropertyNode&) const;
gfx::NativeViewAccessible LineIndexToNode(
const base::string16 line_index) const;
const BrowserAccessibilityCocoa* cocoa_node;
const LineIndexesMap& line_indexes_map;
const NSArray* attributes;
const NSArray* parameterized_attributes;
};
} // namespace a11y } // namespace a11y
} // namespace content } // namespace content
......
...@@ -4,9 +4,51 @@ ...@@ -4,9 +4,51 @@
#include "content/browser/accessibility/accessibility_tree_formatter_utils_mac.h" #include "content/browser/accessibility/accessibility_tree_formatter_utils_mac.h"
#include "base/strings/sys_string_conversions.h"
// error: 'accessibilityAttributeNames' is deprecated: first deprecated in
// macOS 10.10 - Use the NSAccessibility protocol methods instead (see
// NSAccessibilityProtocols.h
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
using base::SysNSStringToUTF16;
namespace content { namespace content {
namespace a11y { namespace a11y {
namespace {
#define INT_FAIL(property_node, msg) \
LOG(ERROR) << "Failed to parse " << property_node.original_property \
<< " to Int: " << msg; \
return nil;
#define INTARRAY_FAIL(property_node, msg) \
LOG(ERROR) << "Failed to parse " << property_node.original_property \
<< " to IntArray: " << msg; \
return nil;
#define NSRANGE_FAIL(property_node, msg) \
LOG(ERROR) << "Failed to parse " << property_node.original_property \
<< " to NSRange: " << msg; \
return nil;
#define UIELEMENT_FAIL(property_node, msg) \
LOG(ERROR) << "Failed to parse " << property_node.original_property \
<< " to UIElement: " << msg; \
return nil;
#define TEXTMARKER_FAIL(property_node, msg) \
LOG(ERROR) << "Failed to parse " << property_node.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
LineIndexesMap::LineIndexesMap(const BrowserAccessibilityCocoa* cocoa_node) { LineIndexesMap::LineIndexesMap(const BrowserAccessibilityCocoa* cocoa_node) {
int counter = 0; int counter = 0;
Build(cocoa_node, &counter); Build(cocoa_node, &counter);
...@@ -42,5 +84,231 @@ void LineIndexesMap::Build(const BrowserAccessibilityCocoa* cocoa_node, ...@@ -42,5 +84,231 @@ void LineIndexesMap::Build(const BrowserAccessibilityCocoa* cocoa_node,
Build(cocoa_child, counter); Build(cocoa_child, counter);
} }
} }
// AttributeInvoker
AttributeInvoker::AttributeInvoker(const BrowserAccessibilityCocoa* cocoa_node,
const LineIndexesMap& line_indexes_map)
: cocoa_node(cocoa_node), line_indexes_map(line_indexes_map) {
attributes = [cocoa_node accessibilityAttributeNames];
parameterized_attributes =
[cocoa_node accessibilityParameterizedAttributeNames];
}
OptionalNSObject AttributeInvoker::Invoke(
const PropertyNode& property_node) const {
// Attributes
for (NSString* attribute : attributes) {
if (property_node.IsMatching(SysNSStringToUTF16(attribute))) {
return OptionalNSObject::NotNullOrNotApplicable(
[cocoa_node accessibilityAttributeValue:attribute]);
}
}
// Parameterized attributes
for (NSString* attribute : parameterized_attributes) {
if (property_node.IsMatching(SysNSStringToUTF16(attribute))) {
OptionalNSObject param = ParamByPropertyNode(property_node);
if (param.IsNotNil()) {
return OptionalNSObject([cocoa_node
accessibilityAttributeValue:attribute
forParameter:*param]);
}
return param;
}
}
return OptionalNSObject::NotApplicable();
}
OptionalNSObject AttributeInvoker::ParamByPropertyNode(
const PropertyNode& property_node) const {
std::string property_name = base::UTF16ToASCII(property_node.name_or_value);
if (property_name == "AXLineForIndex") { // Int
return OptionalNSObject::NotNilOrError(PropertyNodeToInt(property_node));
}
if (property_name == "AXCellForColumnAndRow") { // IntArray
return OptionalNSObject::NotNilOrError(
PropertyNodeToIntArray(property_node));
}
if (property_name == "AXStringForRange") { // NSRange
return OptionalNSObject::NotNilOrError(PropertyNodeToRange(property_node));
}
if (property_name == "AXIndexForChildUIElement") { // UIElement
return OptionalNSObject::NotNilOrError(
PropertyNodeToUIElement(property_node));
}
if (property_name == "AXIndexForTextMarker") { // TextMarker
return OptionalNSObject::NotNilOrError(
PropertyNodeToTextMarker(property_node));
}
if (property_name == "AXStringForTextMarkerRange") { // TextMarkerRange
return OptionalNSObject::NotNilOrError(
PropertyNodeToTextMarkerRange(property_node));
}
return OptionalNSObject::NotApplicable();
}
// NSNumber. Format: integer.
NSNumber* AttributeInvoker::PropertyNodeToInt(
const PropertyNode& property_node) const {
if (property_node.parameters.size() != 1) {
INT_FAIL(property_node, "single argument is expected")
}
const auto& intnode = property_node.parameters[0];
base::Optional<int> param = intnode.AsInt();
if (!param) {
INT_FAIL(property_node, "not a number")
}
return [NSNumber numberWithInt:*param];
}
// NSArray of two NSNumber. Format: [integer, integer].
NSArray* AttributeInvoker::PropertyNodeToIntArray(
const PropertyNode& property_node) const {
if (property_node.parameters.size() != 1) {
INTARRAY_FAIL(property_node, "single argument is expected")
}
const auto& arraynode = property_node.parameters[0];
if (arraynode.name_or_value != base::ASCIIToUTF16("[]")) {
INTARRAY_FAIL(property_node, "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(property_node, paramnode.name_or_value +
base::UTF8ToUTF16(" is not a number"))
}
[array addObject:@(*param)];
}
return array;
}
// NSRange. Format: {loc: integer, len: integer}.
NSValue* AttributeInvoker::PropertyNodeToRange(
const PropertyNode& property_node) const {
if (property_node.parameters.size() != 1) {
NSRANGE_FAIL(property_node, "single argument is expected")
}
const auto& dictnode = property_node.parameters[0];
if (!dictnode.IsDict()) {
NSRANGE_FAIL(property_node, "dictionary is expected")
}
base::Optional<int> loc = dictnode.FindIntKey("loc");
if (!loc) {
NSRANGE_FAIL(property_node, "no loc or loc is not a number")
}
base::Optional<int> len = dictnode.FindIntKey("len");
if (!len) {
NSRANGE_FAIL(property_node, "no len or len is not a number")
}
return [NSValue valueWithRange:NSMakeRange(*loc, *len)];
}
// UIElement. Format: :line_num.
gfx::NativeViewAccessible AttributeInvoker::PropertyNodeToUIElement(
const PropertyNode& property_node) const {
if (property_node.parameters.size() != 1) {
UIELEMENT_FAIL(property_node, "single argument is expected")
}
gfx::NativeViewAccessible uielement = line_indexes_map.NodeBy(
base::UTF16ToUTF8(property_node.parameters[0].name_or_value));
if (!uielement) {
UIELEMENT_FAIL(property_node,
"no corresponding UIElement was found in the tree")
}
return uielement;
}
id AttributeInvoker::DictNodeToTextMarker(const PropertyNode& dictnode) const {
if (!dictnode.IsDict()) {
TEXTMARKER_FAIL(dictnode, "dictionary is expected")
}
if (dictnode.parameters.size() != 3) {
TEXTMARKER_FAIL(dictnode, "wrong number of dictionary elements")
}
BrowserAccessibilityCocoa* anchor_cocoa = line_indexes_map.NodeBy(
base::UTF16ToUTF8(dictnode.parameters[0].name_or_value));
if (!anchor_cocoa) {
TEXTMARKER_FAIL(dictnode, "1st argument: wrong anchor")
}
base::Optional<int> offset = dictnode.parameters[1].AsInt();
if (!offset) {
TEXTMARKER_FAIL(dictnode, "2nd argument: wrong offset")
}
ax::mojom::TextAffinity affinity;
const base::string16& affinity_str = dictnode.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(dictnode, "3rd argument: wrong affinity")
}
return content::AXTextMarkerFrom(anchor_cocoa, *offset, affinity);
}
id AttributeInvoker::PropertyNodeToTextMarker(
const PropertyNode& property_node) const {
if (property_node.parameters.size() != 1) {
TEXTMARKER_FAIL(property_node, "single argument is expected")
}
return DictNodeToTextMarker(property_node.parameters[0]);
}
id AttributeInvoker::PropertyNodeToTextMarkerRange(
const PropertyNode& property_node) const {
if (property_node.parameters.size() != 1) {
TEXTMARKER_FAIL(property_node, "single argument is expected")
}
const auto& rangenode = property_node.parameters[0];
if (!rangenode.IsDict()) {
TEXTMARKER_FAIL(property_node, "dictionary is expected")
}
const PropertyNode* anchornode = rangenode.FindKey("anchor");
if (!anchornode) {
TEXTMARKER_FAIL(property_node, "no anchor")
}
id anchor_textmarker = DictNodeToTextMarker(*anchornode);
if (!anchor_textmarker) {
TEXTMARKER_FAIL(property_node, "failed to parse anchor")
}
const PropertyNode* focusnode = rangenode.FindKey("focus");
if (!focusnode) {
TEXTMARKER_FAIL(property_node, "no focus")
}
id focus_textmarker = DictNodeToTextMarker(*focusnode);
if (!focus_textmarker) {
TEXTMARKER_FAIL(property_node, "failed to parse focus")
}
return content::AXTextMarkerRangeFrom(anchor_textmarker, focus_textmarker);
}
} // namespace a11y } // namespace a11y
} // namespace content } // namespace content
#pragma clang diagnostic pop
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment