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 impl = privates(this).impl;
327 var parentID = GetParentID(this.treeID, this.id);
328 var count = GetChildCount(this.treeID, this.id);
330 for (var i = 0; i < count; ++i) {
331 var childID = GetChildIDAtIndex(this.treeID, this.id, i);
332 childIDs.push(childID);
335 return 'node id=' + impl.id +
336 ' role=' + this.role +
337 ' state=' + $JSON.stringify(this.state) +
338 ' parentID=' + parentID +
339 ' childIds=' + $JSON.stringify(childIDs);
342 dispatchEventAtCapturing_: function(event, path) {
343 privates(event).impl.eventPhase = Event.CAPTURING_PHASE;
344 for (var i = path.length - 1; i >= 0; i--) {
345 this.fireEventListeners_(path[i], event);
346 if (privates(event).impl.propagationStopped)
352 dispatchEventAtTargeting_: function(event) {
353 privates(event).impl.eventPhase = Event.AT_TARGET;
354 this.fireEventListeners_(this.wrapper, event);
355 return !privates(event).impl.propagationStopped;
358 dispatchEventAtBubbling_: function(event, path) {
359 privates(event).impl.eventPhase = Event.BUBBLING_PHASE;
360 for (var i = 0; i < path.length; i++) {
361 this.fireEventListeners_(path[i], event);
362 if (privates(event).impl.propagationStopped)
368 fireEventListeners_: function(node, event) {
369 var nodeImpl = privates(node).impl;
370 var listeners = nodeImpl.listeners[event.type];
373 var eventPhase = event.eventPhase;
374 for (var i = 0; i < listeners.length; i++) {
375 if (eventPhase == Event.CAPTURING_PHASE && !listeners[i].capture)
377 if (eventPhase == Event.BUBBLING_PHASE && listeners[i].capture)
381 listeners[i].callback(event);
383 logging.WARNING('Error in event handler for ' + event.type +
384 ' during phase ' + eventPhase + ': ' +
385 e.message + '\nStack trace: ' + e.stack);
390 performAction_: function(actionType, opt_args) {
391 // Not yet initialized.
392 if (this.rootImpl.treeID === undefined ||
393 this.id === undefined) {
397 // Check permissions.
398 if (!IsInteractPermitted()) {
399 throw new Error(actionType + ' requires {"desktop": true} or' +
400 ' {"interact": true} in the "automation" manifest key.');
403 automationInternal.performAction({ treeID: this.rootImpl.treeID,
404 automationNodeID: this.id,
405 actionType: actionType },
409 domQuerySelectorCallback_: function(userCallback, resultAutomationNodeID) {
410 // resultAutomationNodeID could be zero or undefined or (unlikely) null;
411 // they all amount to the same thing here, which is that no node was
413 if (!resultAutomationNodeID) {
417 var resultNode = this.rootImpl.get(resultAutomationNodeID);
419 logging.WARNING('Query selector result not in tree: ' +
420 resultAutomationNodeID);
423 userCallback(resultNode);
426 findInternal_: function(params, opt_results) {
428 this.forAllDescendants_(function(node) {
429 if (privates(node).impl.matchInternal_(params)) {
431 opt_results.push(node);
443 * Executes a closure for all of this node's descendants, in pre-order.
444 * Early-outs if the closure returns true.
445 * @param {Function(AutomationNode):boolean} closure Closure to be executed
446 * for each node. Return true to early-out the traversal.
448 forAllDescendants_: function(closure) {
449 var stack = this.wrapper.children.reverse();
450 while (stack.length > 0) {
451 var node = stack.pop();
455 var children = node.children;
456 for (var i = children.length - 1; i >= 0; i--)
457 stack.push(children[i]);
461 matchInternal_: function(params) {
462 if (Object.keys(params).length == 0)
465 if ('role' in params && this.role != params.role)
468 if ('state' in params) {
469 for (var state in params.state) {
470 if (params.state[state] != (state in this.state))
474 if ('attributes' in params) {
475 for (var attribute in params.attributes) {
476 var attrValue = params.attributes[attribute];
477 if (typeof attrValue != 'object') {
478 if (this[attribute] !== attrValue)
480 } else if (attrValue instanceof RegExp) {
481 if (typeof this[attribute] != 'string')
483 if (!attrValue.test(this[attribute]))
486 // TODO(aboxhall): handle intlist case.
495 var stringAttributes = [
500 'containerLiveRelevant',
501 'containerLiveStatus',
520 var boolAttributes = [
525 'containerLiveAtomic',
532 'updateLocationOnly'];
534 var intAttributes = [
551 'tableCellColumnIndex',
552 'tableCellColumnSpan',
564 var nodeRefAttributes = [
565 ['activedescendantId', 'activedescendant'],
566 ['anchorObjectId', 'anchorObject'],
567 ['focusObjectId', 'focusObject'],
568 ['tableColumnHeaderId', 'tableColumnHeader'],
569 ['tableHeaderId', 'tableHeader'],
570 ['tableRowHeaderId', 'tableRowHeader'],
571 ['titleUiElement', 'titleUIElement']];
573 var intListAttributes = [
579 var nodeRefListAttributes = [
580 ['cellIds', 'cells'],
581 ['controlsIds', 'controls'],
582 ['describedbyIds', 'describedBy'],
583 ['flowtoIds', 'flowTo'],
584 ['labelledbyIds', 'labelledBy'],
585 ['uniqueCellIds', 'uniqueCells']];
587 var floatAttributes = [
588 'docLoadingProgress',
594 var htmlAttributes = [
595 ['type', 'inputType']];
597 var publicAttributes = [];
599 stringAttributes.forEach(function (attributeName) {
600 publicAttributes.push(attributeName);
601 Object.defineProperty(AutomationNodeImpl.prototype, attributeName, {
603 return GetStringAttribute(this.treeID, this.id, attributeName);
608 boolAttributes.forEach(function (attributeName) {
609 publicAttributes.push(attributeName);
610 Object.defineProperty(AutomationNodeImpl.prototype, attributeName, {
612 return GetBoolAttribute(this.treeID, this.id, attributeName);
617 intAttributes.forEach(function (attributeName) {
618 publicAttributes.push(attributeName);
619 Object.defineProperty(AutomationNodeImpl.prototype, attributeName, {
621 return GetIntAttribute(this.treeID, this.id, attributeName);
626 nodeRefAttributes.forEach(function (params) {
627 var srcAttributeName = params[0];
628 var dstAttributeName = params[1];
629 publicAttributes.push(dstAttributeName);
630 Object.defineProperty(AutomationNodeImpl.prototype, dstAttributeName, {
632 var id = GetIntAttribute(this.treeID, this.id, srcAttributeName);
634 return this.rootImpl.get(id);
641 intListAttributes.forEach(function (attributeName) {
642 publicAttributes.push(attributeName);
643 Object.defineProperty(AutomationNodeImpl.prototype, attributeName, {
645 return GetIntListAttribute(this.treeID, this.id, attributeName);
650 nodeRefListAttributes.forEach(function (params) {
651 var srcAttributeName = params[0];
652 var dstAttributeName = params[1];
653 publicAttributes.push(dstAttributeName);
654 Object.defineProperty(AutomationNodeImpl.prototype, dstAttributeName, {
656 var ids = GetIntListAttribute(this.treeID, this.id, srcAttributeName);
660 for (var i = 0; i < ids.length; ++i) {
661 var node = this.rootImpl.get(ids[i]);
670 floatAttributes.forEach(function (attributeName) {
671 publicAttributes.push(attributeName);
672 Object.defineProperty(AutomationNodeImpl.prototype, attributeName, {
674 return GetFloatAttribute(this.treeID, this.id, attributeName);
679 htmlAttributes.forEach(function (params) {
680 var srcAttributeName = params[0];
681 var dstAttributeName = params[1];
682 publicAttributes.push(dstAttributeName);
683 Object.defineProperty(AutomationNodeImpl.prototype, dstAttributeName, {
685 return GetHtmlAttribute(this.treeID, this.id, srcAttributeName);
691 * AutomationRootNode.
693 * An AutomationRootNode is the javascript end of an AXTree living in the
694 * browser. AutomationRootNode handles unserializing incremental updates from
695 * the source AXTree. Each update contains node data that form a complete tree
696 * after applying the update.
698 * A brief note about ids used through this class. The source AXTree assigns
699 * unique ids per node and we use these ids to build a hash to the actual
700 * AutomationNode object.
701 * Thus, tree traversals amount to a lookup in our hash.
703 * The tree itself is identified by the accessibility tree id of the
704 * renderer widget host.
707 function AutomationRootNodeImpl(treeID) {
708 AutomationNodeImpl.call(this, this);
709 this.treeID = treeID;
710 this.axNodeDataCache_ = {};
713 AutomationRootNodeImpl.idToAutomationRootNode_ = {};
715 AutomationRootNodeImpl.get = function(treeID) {
716 var result = AutomationRootNodeImpl.idToAutomationRootNode_[treeID];
717 return result || undefined;
720 AutomationRootNodeImpl.getOrCreate = function(treeID) {
721 if (AutomationRootNodeImpl.idToAutomationRootNode_[treeID])
722 return AutomationRootNodeImpl.idToAutomationRootNode_[treeID];
723 var result = new AutomationRootNode(treeID);
724 AutomationRootNodeImpl.idToAutomationRootNode_[treeID] = result;
728 AutomationRootNodeImpl.destroy = function(treeID) {
729 delete AutomationRootNodeImpl.idToAutomationRootNode_[treeID];
732 AutomationRootNodeImpl.prototype = {
733 __proto__: AutomationNodeImpl.prototype,
746 * The parent of this node from a different tree.
747 * @type {?AutomationNode}
753 * A map from id to AutomationNode.
754 * @type {Object.<number, AutomationNode>}
757 axNodeDataCache_: null,
760 var result = GetRootID(this.treeID);
762 // Don't return undefined, because the id is often passed directly
763 // as an argument to a native binding that expects only a valid number.
764 if (result === undefined)
777 var obj = this.axNodeDataCache_[id];
781 obj = new AutomationNode(this);
782 privates(obj).impl.treeID = this.treeID;
783 privates(obj).impl.id = id;
784 this.axNodeDataCache_[id] = obj;
789 remove: function(id) {
790 delete this.axNodeDataCache_[id];
793 destroy: function() {
794 this.dispatchEvent(schema.EventType.destroyed);
797 setHostNode(hostNode) {
798 this.hostNode_ = hostNode;
801 onAccessibilityEvent: function(eventParams) {
802 var targetNode = this.get(eventParams.targetID);
804 var targetNodeImpl = privates(targetNode).impl;
805 targetNodeImpl.dispatchEvent(eventParams.eventType);
807 logging.WARNING('Got ' + eventParams.eventType +
808 ' event on unknown node: ' + eventParams.targetID +
809 '; this: ' + this.id);
814 toString: function() {
815 function toStringInternal(node, indent) {
819 new Array(indent).join(' ') +
820 AutomationNodeImpl.prototype.toString.call(node) +
823 for (var i = 0; i < node.children.length; i++)
824 output += toStringInternal(node.children[i], indent);
827 return toStringInternal(this, 0);
831 var AutomationNode = utils.expose('AutomationNode',
833 { functions: ['doDefault',
842 'removeEventListener',
845 readonly: publicAttributes.concat(
859 var AutomationRootNode = utils.expose('AutomationRootNode',
860 AutomationRootNodeImpl,
861 { superclass: AutomationNode });
863 AutomationRootNode.get = function(treeID) {
864 return AutomationRootNodeImpl.get(treeID);
867 AutomationRootNode.getOrCreate = function(treeID) {
868 return AutomationRootNodeImpl.getOrCreate(treeID);
871 AutomationRootNode.destroy = function(treeID) {
872 AutomationRootNodeImpl.destroy(treeID);
875 exports.AutomationNode = AutomationNode;
876 exports.AutomationRootNode = AutomationRootNode;