1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 var AutomationEvent
= require('automationEvent').AutomationEvent
;
6 var automationInternal
=
7 require('binding').Binding
.create('automationInternal').generate();
8 var IsInteractPermitted
=
9 requireNative('automationInternal').IsInteractPermitted
;
11 var lastError
= require('lastError');
12 var logging
= requireNative('logging');
13 var schema
= requireNative('automationInternal').GetSchemaAdditions();
14 var utils
= require('utils');
17 * A single node in the Automation tree.
18 * @param {AutomationRootNodeImpl} root The root of the tree.
21 function AutomationNodeImpl(root
) {
24 // Public attributes. No actual data gets set on this object.
26 // Internal object holding all attributes.
27 this.attributesInternal
= {};
29 this.location
= { left
: 0, top
: 0, width
: 0, height
: 0 };
32 AutomationNodeImpl
.prototype = {
35 state
: { busy
: true },
39 return this.rootImpl
.wrapper
;
43 return this.hostTree
|| this.rootImpl
.get(this.parentID
);
46 firstChild: function() {
47 return this.childTree
|| this.rootImpl
.get(this.childIds
[0]);
50 lastChild: function() {
51 var childIds
= this.childIds
;
52 return this.childTree
|| this.rootImpl
.get(childIds
[childIds
.length
- 1]);
55 children: function() {
57 for (var i
= 0, childID
; childID
= this.childIds
[i
]; i
++) {
58 logging
.CHECK(this.rootImpl
.get(childID
));
59 children
.push(this.rootImpl
.get(childID
));
64 previousSibling: function() {
65 var parent
= this.parent();
66 if (parent
&& this.indexInParent
> 0)
67 return parent
.children()[this.indexInParent
- 1];
71 nextSibling: function() {
72 var parent
= this.parent();
73 if (parent
&& this.indexInParent
< parent
.children().length
)
74 return parent
.children()[this.indexInParent
+ 1];
78 doDefault: function() {
79 this.performAction_('doDefault');
83 this.performAction_('focus');
86 makeVisible: function() {
87 this.performAction_('makeVisible');
90 setSelection: function(startIndex
, endIndex
) {
91 this.performAction_('setSelection',
92 { startIndex
: startIndex
,
93 endIndex
: endIndex
});
96 domQuerySelector: function(selector
, callback
) {
97 automationInternal
.querySelector(
98 { treeID
: this.rootImpl
.treeID
,
99 automationNodeID
: this.id
,
100 selector
: selector
},
101 this.domQuerySelectorCallback_
.bind(this, callback
));
104 find: function(params
) {
105 return this.findInternal_(params
);
108 findAll: function(params
) {
109 return this.findInternal_(params
, []);
112 matches: function(params
) {
113 return this.matchInternal_(params
);
116 addEventListener: function(eventType
, callback
, capture
) {
117 this.removeEventListener(eventType
, callback
);
118 if (!this.listeners
[eventType
])
119 this.listeners
[eventType
] = [];
120 this.listeners
[eventType
].push({callback
: callback
, capture
: !!capture
});
123 // TODO(dtseng/aboxhall): Check this impl against spec.
124 removeEventListener: function(eventType
, callback
) {
125 if (this.listeners
[eventType
]) {
126 var listeners
= this.listeners
[eventType
];
127 for (var i
= 0; i
< listeners
.length
; i
++) {
128 if (callback
=== listeners
[i
].callback
)
129 listeners
.splice(i
, 1);
135 return { treeID
: this.treeID
,
138 attributes
: this.attributes
};
141 dispatchEvent: function(eventType
) {
143 var parent
= this.parent();
146 parent
= parent
.parent();
148 var event
= new AutomationEvent(eventType
, this.wrapper
);
150 // Dispatch the event through the propagation path in three phases:
151 // - capturing: starting from the root and going down to the target's parent
152 // - targeting: dispatching the event on the target itself
153 // - bubbling: starting from the target's parent, going back up to the root.
154 // At any stage, a listener may call stopPropagation() on the event, which
155 // will immediately stop event propagation through this path.
156 if (this.dispatchEventAtCapturing_(event
, path
)) {
157 if (this.dispatchEventAtTargeting_(event
, path
))
158 this.dispatchEventAtBubbling_(event
, path
);
162 toString: function() {
163 var impl
= privates(this).impl
;
166 return 'node id=' + impl
.id
+
167 ' role=' + this.role
+
168 ' state=' + $JSON
.stringify(this.state
) +
169 ' parentID=' + impl
.parentID
+
170 ' childIds=' + $JSON
.stringify(impl
.childIds
) +
171 ' attributes=' + $JSON
.stringify(this.attributes
);
174 dispatchEventAtCapturing_: function(event
, path
) {
175 privates(event
).impl
.eventPhase
= Event
.CAPTURING_PHASE
;
176 for (var i
= path
.length
- 1; i
>= 0; i
--) {
177 this.fireEventListeners_(path
[i
], event
);
178 if (privates(event
).impl
.propagationStopped
)
184 dispatchEventAtTargeting_: function(event
) {
185 privates(event
).impl
.eventPhase
= Event
.AT_TARGET
;
186 this.fireEventListeners_(this.wrapper
, event
);
187 return !privates(event
).impl
.propagationStopped
;
190 dispatchEventAtBubbling_: function(event
, path
) {
191 privates(event
).impl
.eventPhase
= Event
.BUBBLING_PHASE
;
192 for (var i
= 0; i
< path
.length
; i
++) {
193 this.fireEventListeners_(path
[i
], event
);
194 if (privates(event
).impl
.propagationStopped
)
200 fireEventListeners_: function(node
, event
) {
201 var nodeImpl
= privates(node
).impl
;
202 var listeners
= nodeImpl
.listeners
[event
.type
];
205 var eventPhase
= event
.eventPhase
;
206 for (var i
= 0; i
< listeners
.length
; i
++) {
207 if (eventPhase
== Event
.CAPTURING_PHASE
&& !listeners
[i
].capture
)
209 if (eventPhase
== Event
.BUBBLING_PHASE
&& listeners
[i
].capture
)
213 listeners
[i
].callback(event
);
215 console
.error('Error in event handler for ' + event
.type
+
216 'during phase ' + eventPhase
+ ': ' +
217 e
.message
+ '\nStack trace: ' + e
.stack
);
222 performAction_: function(actionType
, opt_args
) {
223 // Not yet initialized.
224 if (this.rootImpl
.treeID
=== undefined ||
225 this.id
=== undefined) {
229 // Check permissions.
230 if (!IsInteractPermitted()) {
231 throw new Error(actionType
+ ' requires {"desktop": true} or' +
232 ' {"interact": true} in the "automation" manifest key.');
235 automationInternal
.performAction({ treeID
: this.rootImpl
.treeID
,
236 automationNodeID
: this.id
,
237 actionType
: actionType
},
241 domQuerySelectorCallback_: function(userCallback
, resultAutomationNodeID
) {
242 // resultAutomationNodeID could be zero or undefined or (unlikely) null;
243 // they all amount to the same thing here, which is that no node was
245 if (!resultAutomationNodeID
) {
249 var resultNode
= this.rootImpl
.get(resultAutomationNodeID
);
251 logging
.WARNING('Query selector result not in tree: ' +
252 resultAutomationNodeID
);
255 userCallback(resultNode
);
258 findInternal_: function(params
, opt_results
) {
260 this.forAllDescendants_(function(node
) {
261 if (privates(node
).impl
.matchInternal_(params
)) {
263 opt_results
.push(node
);
275 * Executes a closure for all of this node's descendants, in pre-order.
276 * Early-outs if the closure returns true.
277 * @param {Function(AutomationNode):boolean} closure Closure to be executed
278 * for each node. Return true to early-out the traversal.
280 forAllDescendants_: function(closure
) {
281 var stack
= this.wrapper
.children().reverse();
282 while (stack
.length
> 0) {
283 var node
= stack
.pop();
287 var children
= node
.children();
288 for (var i
= children
.length
- 1; i
>= 0; i
--)
289 stack
.push(children
[i
]);
293 matchInternal_: function(params
) {
294 if (Object
.keys(params
).length
== 0)
297 if ('role' in params
&& this.role
!= params
.role
)
300 if ('state' in params
) {
301 for (var state
in params
.state
) {
302 if (params
.state
[state
] != (state
in this.state
))
306 if ('attributes' in params
) {
307 for (var attribute
in params
.attributes
) {
308 if (!(attribute
in this.attributesInternal
))
311 var attrValue
= params
.attributes
[attribute
];
312 if (typeof attrValue
!= 'object') {
313 if (this.attributesInternal
[attribute
] !== attrValue
)
315 } else if (attrValue
instanceof RegExp
) {
316 if (typeof this.attributesInternal
[attribute
] != 'string')
318 if (!attrValue
.test(this.attributesInternal
[attribute
]))
321 // TODO(aboxhall): handle intlist case.
330 // Maps an attribute to its default value in an invalidated node.
331 // These attributes are taken directly from the Automation idl.
332 var AutomationAttributeDefaults
= {
336 'location': { left
: 0, top
: 0, width
: 0, height
: 0 }
340 var AutomationAttributeTypes
= [
351 * Maps an attribute name to another attribute who's value is an id or an array
352 * of ids referencing an AutomationNode.
353 * @param {!Object.<string, string>}
356 var ATTRIBUTE_NAME_TO_ATTRIBUTE_ID
= {
357 'aria-activedescendant': 'activedescendantId',
358 'aria-controls': 'controlsIds',
359 'aria-describedby': 'describedbyIds',
360 'aria-flowto': 'flowtoIds',
361 'aria-labelledby': 'labelledbyIds',
362 'aria-owns': 'ownsIds'
366 * A set of attributes ignored in the automation API.
367 * @param {!Object.<string, boolean>}
370 var ATTRIBUTE_BLACKLIST
= {'activedescendantId': true,
373 'describedbyIds': true,
375 'labelledbyIds': true,
381 * AutomationRootNode.
383 * An AutomationRootNode is the javascript end of an AXTree living in the
384 * browser. AutomationRootNode handles unserializing incremental updates from
385 * the source AXTree. Each update contains node data that form a complete tree
386 * after applying the update.
388 * A brief note about ids used through this class. The source AXTree assigns
389 * unique ids per node and we use these ids to build a hash to the actual
390 * AutomationNode object.
391 * Thus, tree traversals amount to a lookup in our hash.
393 * The tree itself is identified by the accessibility tree id of the
394 * renderer widget host.
397 function AutomationRootNodeImpl(treeID
) {
398 AutomationNodeImpl
.call(this, this);
399 this.treeID
= treeID
;
400 this.axNodeDataCache_
= {};
403 AutomationRootNodeImpl
.prototype = {
404 __proto__
: AutomationNodeImpl
.prototype,
412 return this.axNodeDataCache_
[id
];
415 unserialize: function(update
) {
416 var updateState
= { pendingNodes
: {}, newNodes
: {} };
417 var oldRootId
= this.id
;
419 if (update
.nodeIdToClear
< 0) {
420 logging
.WARNING('Bad nodeIdToClear: ' + update
.nodeIdToClear
);
421 lastError
.set('automation',
422 'Bad update received on automation tree',
426 } else if (update
.nodeIdToClear
> 0) {
427 var nodeToClear
= this.axNodeDataCache_
[update
.nodeIdToClear
];
429 logging
.WARNING('Bad nodeIdToClear: ' + update
.nodeIdToClear
+
431 lastError
.set('automation',
432 'Bad update received on automation tree',
437 if (nodeToClear
=== this.wrapper
) {
438 this.invalidate_(nodeToClear
);
440 var children
= nodeToClear
.children();
441 for (var i
= 0; i
< children
.length
; i
++)
442 this.invalidate_(children
[i
]);
443 var nodeToClearImpl
= privates(nodeToClear
).impl
;
444 nodeToClearImpl
.childIds
= []
445 updateState
.pendingNodes
[nodeToClearImpl
.id
] = nodeToClear
;
449 for (var i
= 0; i
< update
.nodes
.length
; i
++) {
450 if (!this.updateNode_(update
.nodes
[i
], updateState
))
454 if (Object
.keys(updateState
.pendingNodes
).length
> 0) {
455 logging
.WARNING('Nodes left pending by the update: ' +
456 $JSON
.stringify(updateState
.pendingNodes
));
457 lastError
.set('automation',
458 'Bad update received on automation tree',
466 destroy: function() {
468 this.hostTree
.childTree
= undefined;
469 this.hostTree
= undefined;
471 this.dispatchEvent(schema
.EventType
.destroyed
);
472 this.invalidate_(this.wrapper
);
475 onAccessibilityEvent: function(eventParams
) {
476 if (!this.unserialize(eventParams
.update
)) {
477 logging
.WARNING('unserialization failed');
481 var targetNode
= this.get(eventParams
.targetID
);
483 var targetNodeImpl
= privates(targetNode
).impl
;
484 targetNodeImpl
.dispatchEvent(eventParams
.eventType
);
486 logging
.WARNING('Got ' + eventParams
.eventType
+
487 ' event on unknown node: ' + eventParams
.targetID
+
488 '; this: ' + this.id
);
493 toString: function() {
494 function toStringInternal(node
, indent
) {
498 new Array(indent
).join(' ') +
499 AutomationNodeImpl
.prototype.toString
.call(node
) +
502 for (var i
= 0; i
< node
.children().length
; i
++)
503 output
+= toStringInternal(node
.children()[i
], indent
);
506 return toStringInternal(this, 0);
509 invalidate_: function(node
) {
512 var children
= node
.children();
514 for (var i
= 0, child
; child
= children
[i
]; i
++)
515 this.invalidate_(child
);
517 // Retrieve the internal AutomationNodeImpl instance for this node.
518 // This object is not accessible outside of bindings code, but we can access
520 var nodeImpl
= privates(node
).impl
;
521 var id
= nodeImpl
.id
;
522 for (var key
in AutomationAttributeDefaults
) {
523 nodeImpl
[key
] = AutomationAttributeDefaults
[key
];
525 nodeImpl
.childIds
= [];
527 delete this.axNodeDataCache_
[id
];
530 deleteOldChildren_: function(node
, newChildIds
) {
531 // Create a set of child ids in |src| for fast lookup, and return false
532 // if a duplicate is found;
533 var newChildIdSet
= {};
534 for (var i
= 0; i
< newChildIds
.length
; i
++) {
535 var childId
= newChildIds
[i
];
536 if (newChildIdSet
[childId
]) {
537 logging
.WARNING('Node ' + privates(node
).impl
.id
+
538 ' has duplicate child id ' + childId
);
539 lastError
.set('automation',
540 'Bad update received on automation tree',
545 newChildIdSet
[newChildIds
[i
]] = true;
548 // Delete the old children.
549 var nodeImpl
= privates(node
).impl
;
550 var oldChildIds
= nodeImpl
.childIds
;
551 for (var i
= 0; i
< oldChildIds
.length
;) {
552 var oldId
= oldChildIds
[i
];
553 if (!newChildIdSet
[oldId
]) {
554 this.invalidate_(this.axNodeDataCache_
[oldId
]);
555 oldChildIds
.splice(i
, 1);
560 nodeImpl
.childIds
= oldChildIds
;
565 createNewChildren_: function(node
, newChildIds
, updateState
) {
569 for (var i
= 0; i
< newChildIds
.length
; i
++) {
570 var childId
= newChildIds
[i
];
571 var childNode
= this.axNodeDataCache_
[childId
];
573 if (childNode
.parent() != node
) {
575 if (childNode
.parent()) {
576 var parentImpl
= privates(childNode
.parent()).impl
;
577 parentId
= parentImpl
.id
;
579 // This is a serious error - nodes should never be reparented.
580 // If this case occurs, continue so this node isn't left in an
581 // inconsistent state, but return failure at the end.
582 logging
.WARNING('Node ' + childId
+ ' reparented from ' +
583 parentId
+ ' to ' + privates(node
).impl
.id
);
584 lastError
.set('automation',
585 'Bad update received on automation tree',
592 childNode
= new AutomationNode(this);
593 this.axNodeDataCache_
[childId
] = childNode
;
594 privates(childNode
).impl
.id
= childId
;
595 updateState
.pendingNodes
[childId
] = childNode
;
596 updateState
.newNodes
[childId
] = childNode
;
598 privates(childNode
).impl
.indexInParent
= i
;
599 privates(childNode
).impl
.parentID
= privates(node
).impl
.id
;
605 setData_: function(node
, nodeData
) {
606 var nodeImpl
= privates(node
).impl
;
608 // TODO(dtseng): Make into set listing all hosting node roles.
609 if (nodeData
.role
== schema
.RoleType
.webView
) {
610 if (nodeImpl
.pendingChildFrame
=== undefined)
611 nodeImpl
.pendingChildFrame
= true;
613 if (nodeImpl
.pendingChildFrame
) {
614 nodeImpl
.childTreeID
= nodeData
.intAttributes
.childTreeId
;
615 automationInternal
.enableFrame(nodeImpl
.childTreeID
);
616 automationUtil
.storeTreeCallback(nodeImpl
.childTreeID
, function(root
) {
617 nodeImpl
.pendingChildFrame
= false;
618 nodeImpl
.childTree
= root
;
619 privates(root
).impl
.hostTree
= node
;
620 nodeImpl
.dispatchEvent(schema
.EventType
.childrenChanged
);
624 for (var key
in AutomationAttributeDefaults
) {
626 nodeImpl
[key
] = nodeData
[key
];
628 nodeImpl
[key
] = AutomationAttributeDefaults
[key
];
630 for (var i
= 0; i
< AutomationAttributeTypes
.length
; i
++) {
631 var attributeType
= AutomationAttributeTypes
[i
];
632 for (var attributeName
in nodeData
[attributeType
]) {
633 nodeImpl
.attributesInternal
[attributeName
] =
634 nodeData
[attributeType
][attributeName
];
635 if (ATTRIBUTE_BLACKLIST
.hasOwnProperty(attributeName
) ||
636 nodeImpl
.attributes
.hasOwnProperty(attributeName
)) {
639 ATTRIBUTE_NAME_TO_ATTRIBUTE_ID
.hasOwnProperty(attributeName
)) {
640 this.defineReadonlyAttribute_(nodeImpl
,
644 this.defineReadonlyAttribute_(nodeImpl
,
651 defineReadonlyAttribute_: function(node
, attributeName
, opt_isIDRef
) {
652 $Object
.defineProperty(node
.attributes
, attributeName
, {
656 var attributeId
= node
.attributesInternal
[
657 ATTRIBUTE_NAME_TO_ATTRIBUTE_ID
[attributeName
]];
658 if (Array
.isArray(attributeId
)) {
659 return attributeId
.map(function(current
) {
660 return node
.rootImpl
.get(current
);
663 return node
.rootImpl
.get(attributeId
);
665 return node
.attributesInternal
[attributeName
];
670 updateNode_: function(nodeData
, updateState
) {
671 var node
= this.axNodeDataCache_
[nodeData
.id
];
672 var didUpdateRoot
= false;
674 delete updateState
.pendingNodes
[privates(node
).impl
.id
];
676 if (nodeData
.role
!= schema
.RoleType
.rootWebArea
&&
677 nodeData
.role
!= schema
.RoleType
.desktop
) {
678 logging
.WARNING(String(nodeData
.id
) +
679 ' is not in the cache and not the new root.');
680 lastError
.set('automation',
681 'Bad update received on automation tree',
686 // |this| is an AutomationRootNodeImpl; retrieve the
687 // AutomationRootNode instance instead.
689 didUpdateRoot
= true;
690 updateState
.newNodes
[this.id
] = this.wrapper
;
692 this.setData_(node
, nodeData
);
694 // TODO(aboxhall): send onChanged event?
696 if (!this.deleteOldChildren_(node
, nodeData
.childIds
)) {
698 this.invalidate_(this.wrapper
);
702 var nodeImpl
= privates(node
).impl
;
704 var success
= this.createNewChildren_(node
,
707 nodeImpl
.childIds
= nodeData
.childIds
;
708 this.axNodeDataCache_
[nodeImpl
.id
] = node
;
715 var AutomationNode
= utils
.expose('AutomationNode',
717 { functions
: ['parent',
731 'removeEventListener',
734 readonly
: ['isRootNode',
742 var AutomationRootNode
= utils
.expose('AutomationRootNode',
743 AutomationRootNodeImpl
,
744 { superclass
: AutomationNode
});
746 exports
.AutomationNode
= AutomationNode
;
747 exports
.AutomationRootNode
= AutomationRootNode
;