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
;
12 * @param {number} axTreeID The id of the accessibility tree.
13 * @return {?number} The id of the root node.
15 var GetRootID
= requireNative('automationInternal').GetRootID
;
18 * @param {number} axTreeID The id of the accessibility tree.
19 * @param {number} nodeID The id of a node.
20 * @return {?number} The id of the node's parent, or undefined if it's the
21 * root of its tree or if the tree or node wasn't found.
23 var GetParentID
= requireNative('automationInternal').GetParentID
;
26 * @param {number} axTreeID The id of the accessibility tree.
27 * @param {number} nodeID The id of a node.
28 * @return {?number} The number of children of the node, or undefined if
29 * the tree or node wasn't found.
31 var GetChildCount
= requireNative('automationInternal').GetChildCount
;
34 * @param {number} axTreeID The id of the accessibility tree.
35 * @param {number} nodeID The id of a node.
36 * @param {number} childIndex An index of a child of this node.
37 * @return {?number} The id of the child at the given index, or undefined
38 * if the tree or node or child at that index wasn't found.
40 var GetChildIDAtIndex
= requireNative('automationInternal').GetChildIDAtIndex
;
43 * @param {number} axTreeID The id of the accessibility tree.
44 * @param {number} nodeID The id of a node.
45 * @return {?number} The index of this node in its parent, or undefined if
46 * the tree or node or node parent wasn't found.
48 var GetIndexInParent
= requireNative('automationInternal').GetIndexInParent
;
51 * @param {number} axTreeID The id of the accessibility tree.
52 * @param {number} nodeID The id of a node.
53 * @return {?Object} An object with a string key for every state flag set,
54 * or undefined if the tree or node or node parent wasn't found.
56 var GetState
= requireNative('automationInternal').GetState
;
59 * @param {number} axTreeID The id of the accessibility tree.
60 * @param {number} nodeID The id of a node.
61 * @return {string} The role of the node, or undefined if the tree or
64 var GetRole
= requireNative('automationInternal').GetRole
;
67 * @param {number} axTreeID The id of the accessibility tree.
68 * @param {number} nodeID The id of a node.
69 * @return {?automation.Rect} The location of the node, or undefined if
70 * the tree or node wasn't found.
72 var GetLocation
= requireNative('automationInternal').GetLocation
;
75 * @param {number} axTreeID The id of the accessibility tree.
76 * @param {number} nodeID The id of a node.
77 * @param {string} attr The name of a string attribute.
78 * @return {?string} The value of this attribute, or undefined if the tree,
79 * node, or attribute wasn't found.
81 var GetStringAttribute
= requireNative('automationInternal').GetStringAttribute
;
84 * @param {number} axTreeID The id of the accessibility tree.
85 * @param {number} nodeID The id of a node.
86 * @param {string} attr The name of an attribute.
87 * @return {?boolean} The value of this attribute, or undefined if the tree,
88 * node, or attribute wasn't found.
90 var GetBoolAttribute
= requireNative('automationInternal').GetBoolAttribute
;
93 * @param {number} axTreeID The id of the accessibility tree.
94 * @param {number} nodeID The id of a node.
95 * @param {string} attr The name of an attribute.
96 * @return {?number} The value of this attribute, or undefined if the tree,
97 * node, or attribute wasn't found.
99 var GetIntAttribute
= requireNative('automationInternal').GetIntAttribute
;
102 * @param {number} axTreeID The id of the accessibility tree.
103 * @param {number} nodeID The id of a node.
104 * @param {string} attr The name of an attribute.
105 * @return {?number} The value of this attribute, or undefined if the tree,
106 * node, or attribute wasn't found.
108 var GetFloatAttribute
= requireNative('automationInternal').GetFloatAttribute
;
111 * @param {number} axTreeID The id of the accessibility tree.
112 * @param {number} nodeID The id of a node.
113 * @param {string} attr The name of an attribute.
114 * @return {?Array.<number>} The value of this attribute, or undefined
115 * if the tree, node, or attribute wasn't found.
117 var GetIntListAttribute
=
118 requireNative('automationInternal').GetIntListAttribute
;
121 * @param {number} axTreeID The id of the accessibility tree.
122 * @param {number} nodeID The id of a node.
123 * @param {string} attr The name of an HTML attribute.
124 * @return {?string} The value of this attribute, or undefined if the tree,
125 * node, or attribute wasn't found.
127 var GetHtmlAttribute
= requireNative('automationInternal').GetHtmlAttribute
;
129 var lastError
= require('lastError');
130 var logging
= requireNative('logging');
131 var schema
= requireNative('automationInternal').GetSchemaAdditions();
132 var utils
= require('utils');
135 * A single node in the Automation tree.
136 * @param {AutomationRootNodeImpl} root The root of the tree.
139 function AutomationNodeImpl(root
) {
140 this.rootImpl
= root
;
141 // Public attributes. No actual data gets set on this object.
145 AutomationNodeImpl
.prototype = {
149 state
: { busy
: true },
153 return this.rootImpl
.wrapper
;
158 return this.hostNode_
;
159 var parentID
= GetParentID(this.treeID
, this.id
);
160 return this.rootImpl
.get(parentID
);
164 return GetState(this.treeID
, this.id
);
168 return GetRole(this.treeID
, this.id
);
172 return GetLocation(this.treeID
, this.id
);
175 get indexInParent() {
176 return GetIndexInParent(this.treeID
, this.id
);
180 var childTreeID
= GetIntAttribute(this.treeID
, this.id
, 'childTreeId');
182 return AutomationRootNodeImpl
.get(childTreeID
);
187 return this.childTree
;
188 if (!GetChildCount(this.treeID
, this.id
))
190 var firstChildID
= GetChildIDAtIndex(this.treeID
, this.id
, 0);
191 return this.rootImpl
.get(firstChildID
);
196 return this.childTree
;
197 var count
= GetChildCount(this.treeID
, this.id
);
200 var lastChildID
= GetChildIDAtIndex(this.treeID
, this.id
, count
- 1);
201 return this.rootImpl
.get(lastChildID
);
206 return [this.childTree
];
209 var count
= GetChildCount(this.treeID
, this.id
);
210 for (var i
= 0; i
< count
; ++i
) {
211 var childID
= GetChildIDAtIndex(this.treeID
, this.id
, i
);
212 var child
= this.rootImpl
.get(childID
);
213 children
.push(child
);
218 get previousSibling() {
219 var parent
= this.parent
;
220 var indexInParent
= GetIndexInParent(this.treeID
, this.id
);
221 if (parent
&& indexInParent
> 0)
222 return parent
.children
[indexInParent
- 1];
227 var parent
= this.parent
;
228 var indexInParent
= GetIndexInParent(this.treeID
, this.id
);
229 if (parent
&& indexInParent
< parent
.children
.length
)
230 return parent
.children
[indexInParent
+ 1];
234 doDefault: function() {
235 this.performAction_('doDefault');
239 this.performAction_('focus');
242 makeVisible: function() {
243 this.performAction_('makeVisible');
246 setSelection: function(startIndex
, endIndex
) {
247 this.performAction_('setSelection',
248 { startIndex
: startIndex
,
249 endIndex
: endIndex
});
252 showContextMenu: function() {
253 this.performAction_('showContextMenu');
256 domQuerySelector: function(selector
, callback
) {
257 automationInternal
.querySelector(
258 { treeID
: this.rootImpl
.treeID
,
259 automationNodeID
: this.id
,
260 selector
: selector
},
261 this.domQuerySelectorCallback_
.bind(this, callback
));
264 find: function(params
) {
265 return this.findInternal_(params
);
268 findAll: function(params
) {
269 return this.findInternal_(params
, []);
272 matches: function(params
) {
273 return this.matchInternal_(params
);
276 addEventListener: function(eventType
, callback
, capture
) {
277 this.removeEventListener(eventType
, callback
);
278 if (!this.listeners
[eventType
])
279 this.listeners
[eventType
] = [];
280 this.listeners
[eventType
].push({callback
: callback
, capture
: !!capture
});
283 // TODO(dtseng/aboxhall): Check this impl against spec.
284 removeEventListener: function(eventType
, callback
) {
285 if (this.listeners
[eventType
]) {
286 var listeners
= this.listeners
[eventType
];
287 for (var i
= 0; i
< listeners
.length
; i
++) {
288 if (callback
=== listeners
[i
].callback
)
289 listeners
.splice(i
, 1);
295 return { treeID
: this.treeID
,
298 attributes
: this.attributes
};
301 dispatchEvent: function(eventType
) {
303 var parent
= this.parent
;
306 parent
= parent
.parent
;
308 var event
= new AutomationEvent(eventType
, this.wrapper
);
310 // Dispatch the event through the propagation path in three phases:
311 // - capturing: starting from the root and going down to the target's parent
312 // - targeting: dispatching the event on the target itself
313 // - bubbling: starting from the target's parent, going back up to the root.
314 // At any stage, a listener may call stopPropagation() on the event, which
315 // will immediately stop event propagation through this path.
316 if (this.dispatchEventAtCapturing_(event
, path
)) {
317 if (this.dispatchEventAtTargeting_(event
, path
))
318 this.dispatchEventAtBubbling_(event
, path
);
322 toString: function() {
323 var parentID
= GetParentID(this.treeID
, this.id
);
324 var childTreeID
= GetIntAttribute(this.treeID
, this.id
, 'childTreeId');
325 var count
= GetChildCount(this.treeID
, this.id
);
327 for (var i
= 0; i
< count
; ++i
) {
328 var childID
= GetChildIDAtIndex(this.treeID
, this.id
, i
);
329 childIDs
.push(childID
);
332 var result
= 'node id=' + this.id
+
333 ' role=' + this.role
+
334 ' state=' + $JSON
.stringify(this.state
) +
335 ' parentID=' + parentID
+
336 ' childIds=' + $JSON
.stringify(childIDs
);
337 if (this.hostNode_
) {
338 var hostNodeImpl
= privates(this.hostNode_
).impl
;
339 result
+= ' host treeID=' + hostNodeImpl
.treeID
+
340 ' host nodeID=' + hostNodeImpl
.id
;
343 result
+= ' childTreeID=' + childTreeID
;
347 dispatchEventAtCapturing_: function(event
, path
) {
348 privates(event
).impl
.eventPhase
= Event
.CAPTURING_PHASE
;
349 for (var i
= path
.length
- 1; i
>= 0; i
--) {
350 this.fireEventListeners_(path
[i
], event
);
351 if (privates(event
).impl
.propagationStopped
)
357 dispatchEventAtTargeting_: function(event
) {
358 privates(event
).impl
.eventPhase
= Event
.AT_TARGET
;
359 this.fireEventListeners_(this.wrapper
, event
);
360 return !privates(event
).impl
.propagationStopped
;
363 dispatchEventAtBubbling_: function(event
, path
) {
364 privates(event
).impl
.eventPhase
= Event
.BUBBLING_PHASE
;
365 for (var i
= 0; i
< path
.length
; i
++) {
366 this.fireEventListeners_(path
[i
], event
);
367 if (privates(event
).impl
.propagationStopped
)
373 fireEventListeners_: function(node
, event
) {
374 var nodeImpl
= privates(node
).impl
;
375 var listeners
= nodeImpl
.listeners
[event
.type
];
378 var eventPhase
= event
.eventPhase
;
379 for (var i
= 0; i
< listeners
.length
; i
++) {
380 if (eventPhase
== Event
.CAPTURING_PHASE
&& !listeners
[i
].capture
)
382 if (eventPhase
== Event
.BUBBLING_PHASE
&& listeners
[i
].capture
)
386 listeners
[i
].callback(event
);
388 logging
.WARNING('Error in event handler for ' + event
.type
+
389 ' during phase ' + eventPhase
+ ': ' +
390 e
.message
+ '\nStack trace: ' + e
.stack
);
395 performAction_: function(actionType
, opt_args
) {
396 // Not yet initialized.
397 if (this.rootImpl
.treeID
=== undefined ||
398 this.id
=== undefined) {
402 // Check permissions.
403 if (!IsInteractPermitted()) {
404 throw new Error(actionType
+ ' requires {"desktop": true} or' +
405 ' {"interact": true} in the "automation" manifest key.');
408 automationInternal
.performAction({ treeID
: this.rootImpl
.treeID
,
409 automationNodeID
: this.id
,
410 actionType
: actionType
},
414 domQuerySelectorCallback_: function(userCallback
, resultAutomationNodeID
) {
415 // resultAutomationNodeID could be zero or undefined or (unlikely) null;
416 // they all amount to the same thing here, which is that no node was
418 if (!resultAutomationNodeID
) {
422 var resultNode
= this.rootImpl
.get(resultAutomationNodeID
);
424 logging
.WARNING('Query selector result not in tree: ' +
425 resultAutomationNodeID
);
428 userCallback(resultNode
);
431 findInternal_: function(params
, opt_results
) {
433 this.forAllDescendants_(function(node
) {
434 if (privates(node
).impl
.matchInternal_(params
)) {
436 opt_results
.push(node
);
448 * Executes a closure for all of this node's descendants, in pre-order.
449 * Early-outs if the closure returns true.
450 * @param {Function(AutomationNode):boolean} closure Closure to be executed
451 * for each node. Return true to early-out the traversal.
453 forAllDescendants_: function(closure
) {
454 var stack
= this.wrapper
.children
.reverse();
455 while (stack
.length
> 0) {
456 var node
= stack
.pop();
460 var children
= node
.children
;
461 for (var i
= children
.length
- 1; i
>= 0; i
--)
462 stack
.push(children
[i
]);
466 matchInternal_: function(params
) {
467 if (Object
.keys(params
).length
== 0)
470 if ('role' in params
&& this.role
!= params
.role
)
473 if ('state' in params
) {
474 for (var state
in params
.state
) {
475 if (params
.state
[state
] != (state
in this.state
))
479 if ('attributes' in params
) {
480 for (var attribute
in params
.attributes
) {
481 var attrValue
= params
.attributes
[attribute
];
482 if (typeof attrValue
!= 'object') {
483 if (this[attribute
] !== attrValue
)
485 } else if (attrValue
instanceof RegExp
) {
486 if (typeof this[attribute
] != 'string')
488 if (!attrValue
.test(this[attribute
]))
491 // TODO(aboxhall): handle intlist case.
500 var stringAttributes
= [
505 'containerLiveRelevant',
506 'containerLiveStatus',
525 var boolAttributes
= [
530 'containerLiveAtomic',
537 'updateLocationOnly'];
539 var intAttributes
= [
556 'tableCellColumnIndex',
557 'tableCellColumnSpan',
569 var nodeRefAttributes
= [
570 ['activedescendantId', 'activedescendant'],
571 ['anchorObjectId', 'anchorObject'],
572 ['focusObjectId', 'focusObject'],
573 ['tableColumnHeaderId', 'tableColumnHeader'],
574 ['tableHeaderId', 'tableHeader'],
575 ['tableRowHeaderId', 'tableRowHeader'],
576 ['titleUiElement', 'titleUIElement']];
578 var intListAttributes
= [
584 var nodeRefListAttributes
= [
585 ['cellIds', 'cells'],
586 ['controlsIds', 'controls'],
587 ['describedbyIds', 'describedBy'],
588 ['flowtoIds', 'flowTo'],
589 ['labelledbyIds', 'labelledBy'],
590 ['uniqueCellIds', 'uniqueCells']];
592 var floatAttributes
= [
593 'docLoadingProgress',
599 var htmlAttributes
= [
600 ['type', 'inputType']];
602 var publicAttributes
= [];
604 stringAttributes
.forEach(function (attributeName
) {
605 publicAttributes
.push(attributeName
);
606 Object
.defineProperty(AutomationNodeImpl
.prototype, attributeName
, {
608 return GetStringAttribute(this.treeID
, this.id
, attributeName
);
613 boolAttributes
.forEach(function (attributeName
) {
614 publicAttributes
.push(attributeName
);
615 Object
.defineProperty(AutomationNodeImpl
.prototype, attributeName
, {
617 return GetBoolAttribute(this.treeID
, this.id
, attributeName
);
622 intAttributes
.forEach(function (attributeName
) {
623 publicAttributes
.push(attributeName
);
624 Object
.defineProperty(AutomationNodeImpl
.prototype, attributeName
, {
626 return GetIntAttribute(this.treeID
, this.id
, attributeName
);
631 nodeRefAttributes
.forEach(function (params
) {
632 var srcAttributeName
= params
[0];
633 var dstAttributeName
= params
[1];
634 publicAttributes
.push(dstAttributeName
);
635 Object
.defineProperty(AutomationNodeImpl
.prototype, dstAttributeName
, {
637 var id
= GetIntAttribute(this.treeID
, this.id
, srcAttributeName
);
639 return this.rootImpl
.get(id
);
646 intListAttributes
.forEach(function (attributeName
) {
647 publicAttributes
.push(attributeName
);
648 Object
.defineProperty(AutomationNodeImpl
.prototype, attributeName
, {
650 return GetIntListAttribute(this.treeID
, this.id
, attributeName
);
655 nodeRefListAttributes
.forEach(function (params
) {
656 var srcAttributeName
= params
[0];
657 var dstAttributeName
= params
[1];
658 publicAttributes
.push(dstAttributeName
);
659 Object
.defineProperty(AutomationNodeImpl
.prototype, dstAttributeName
, {
661 var ids
= GetIntListAttribute(this.treeID
, this.id
, srcAttributeName
);
665 for (var i
= 0; i
< ids
.length
; ++i
) {
666 var node
= this.rootImpl
.get(ids
[i
]);
675 floatAttributes
.forEach(function (attributeName
) {
676 publicAttributes
.push(attributeName
);
677 Object
.defineProperty(AutomationNodeImpl
.prototype, attributeName
, {
679 return GetFloatAttribute(this.treeID
, this.id
, attributeName
);
684 htmlAttributes
.forEach(function (params
) {
685 var srcAttributeName
= params
[0];
686 var dstAttributeName
= params
[1];
687 publicAttributes
.push(dstAttributeName
);
688 Object
.defineProperty(AutomationNodeImpl
.prototype, dstAttributeName
, {
690 return GetHtmlAttribute(this.treeID
, this.id
, srcAttributeName
);
696 * AutomationRootNode.
698 * An AutomationRootNode is the javascript end of an AXTree living in the
699 * browser. AutomationRootNode handles unserializing incremental updates from
700 * the source AXTree. Each update contains node data that form a complete tree
701 * after applying the update.
703 * A brief note about ids used through this class. The source AXTree assigns
704 * unique ids per node and we use these ids to build a hash to the actual
705 * AutomationNode object.
706 * Thus, tree traversals amount to a lookup in our hash.
708 * The tree itself is identified by the accessibility tree id of the
709 * renderer widget host.
712 function AutomationRootNodeImpl(treeID
) {
713 AutomationNodeImpl
.call(this, this);
714 this.treeID
= treeID
;
715 this.axNodeDataCache_
= {};
718 AutomationRootNodeImpl
.idToAutomationRootNode_
= {};
720 AutomationRootNodeImpl
.get = function(treeID
) {
721 var result
= AutomationRootNodeImpl
.idToAutomationRootNode_
[treeID
];
722 return result
|| undefined;
725 AutomationRootNodeImpl
.getOrCreate = function(treeID
) {
726 if (AutomationRootNodeImpl
.idToAutomationRootNode_
[treeID
])
727 return AutomationRootNodeImpl
.idToAutomationRootNode_
[treeID
];
728 var result
= new AutomationRootNode(treeID
);
729 AutomationRootNodeImpl
.idToAutomationRootNode_
[treeID
] = result
;
733 AutomationRootNodeImpl
.destroy = function(treeID
) {
734 delete AutomationRootNodeImpl
.idToAutomationRootNode_
[treeID
];
737 AutomationRootNodeImpl
.prototype = {
738 __proto__
: AutomationNodeImpl
.prototype,
751 * The parent of this node from a different tree.
752 * @type {?AutomationNode}
758 * A map from id to AutomationNode.
759 * @type {Object.<number, AutomationNode>}
762 axNodeDataCache_
: null,
765 var result
= GetRootID(this.treeID
);
767 // Don't return undefined, because the id is often passed directly
768 // as an argument to a native binding that expects only a valid number.
769 if (result
=== undefined)
782 var obj
= this.axNodeDataCache_
[id
];
786 obj
= new AutomationNode(this);
787 privates(obj
).impl
.treeID
= this.treeID
;
788 privates(obj
).impl
.id
= id
;
789 this.axNodeDataCache_
[id
] = obj
;
794 remove: function(id
) {
795 delete this.axNodeDataCache_
[id
];
798 destroy: function() {
799 this.dispatchEvent(schema
.EventType
.destroyed
);
802 setHostNode(hostNode
) {
803 this.hostNode_
= hostNode
;
806 onAccessibilityEvent: function(eventParams
) {
807 var targetNode
= this.get(eventParams
.targetID
);
809 var targetNodeImpl
= privates(targetNode
).impl
;
810 targetNodeImpl
.dispatchEvent(eventParams
.eventType
);
812 logging
.WARNING('Got ' + eventParams
.eventType
+
813 ' event on unknown node: ' + eventParams
.targetID
+
814 '; this: ' + this.id
);
819 toString: function() {
820 function toStringInternal(nodeImpl
, indent
) {
824 if (nodeImpl
.isRootNode
)
825 output
+= indent
+ 'tree id=' + nodeImpl
.treeID
+ '\n';
827 AutomationNodeImpl
.prototype.toString
.call(nodeImpl
) +
830 var children
= nodeImpl
.children
;
831 for (var i
= 0; i
< children
.length
; ++i
)
832 output
+= toStringInternal(privates(children
[i
]).impl
, indent
);
835 return toStringInternal(this, '');
839 var AutomationNode
= utils
.expose('AutomationNode',
841 { functions
: ['doDefault',
850 'removeEventListener',
853 readonly
: publicAttributes
.concat(
867 var AutomationRootNode
= utils
.expose('AutomationRootNode',
868 AutomationRootNodeImpl
,
869 { superclass
: AutomationNode
});
871 AutomationRootNode
.get = function(treeID
) {
872 return AutomationRootNodeImpl
.get(treeID
);
875 AutomationRootNode
.getOrCreate = function(treeID
) {
876 return AutomationRootNodeImpl
.getOrCreate(treeID
);
879 AutomationRootNode
.destroy = function(treeID
) {
880 AutomationRootNodeImpl
.destroy(treeID
);
883 exports
.AutomationNode
= AutomationNode
;
884 exports
.AutomationRootNode
= AutomationRootNode
;