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 querySelector: function(selector, callback) {
97 automationInternal.querySelector(
98 { treeID: this.rootImpl.treeID,
99 automationNodeID: this.id,
100 selector: selector },
101 this.querySelectorCallback_.bind(this, callback));
104 addEventListener: function(eventType, callback, capture) {
105 this.removeEventListener(eventType, callback);
106 if (!this.listeners[eventType])
107 this.listeners[eventType] = [];
108 this.listeners[eventType].push({callback: callback, capture: !!capture});
111 // TODO(dtseng/aboxhall): Check this impl against spec.
112 removeEventListener: function(eventType, callback) {
113 if (this.listeners[eventType]) {
114 var listeners = this.listeners[eventType];
115 for (var i = 0; i < listeners.length; i++) {
116 if (callback === listeners[i].callback)
117 listeners.splice(i, 1);
123 return { treeID: this.treeID,
126 attributes: this.attributes };
129 dispatchEvent: function(eventType) {
131 var parent = this.parent();
134 parent = parent.parent();
136 var event = new AutomationEvent(eventType, this.wrapper);
138 // Dispatch the event through the propagation path in three phases:
139 // - capturing: starting from the root and going down to the target's parent
140 // - targeting: dispatching the event on the target itself
141 // - bubbling: starting from the target's parent, going back up to the root.
142 // At any stage, a listener may call stopPropagation() on the event, which
143 // will immediately stop event propagation through this path.
144 if (this.dispatchEventAtCapturing_(event, path)) {
145 if (this.dispatchEventAtTargeting_(event, path))
146 this.dispatchEventAtBubbling_(event, path);
150 toString: function() {
151 var impl = privates(this).impl;
154 return 'node id=' + impl.id +
155 ' role=' + this.role +
156 ' state=' + $JSON.stringify(this.state) +
157 ' parentID=' + impl.parentID +
158 ' childIds=' + $JSON.stringify(impl.childIds) +
159 ' attributes=' + $JSON.stringify(this.attributes);
162 dispatchEventAtCapturing_: function(event, path) {
163 privates(event).impl.eventPhase = Event.CAPTURING_PHASE;
164 for (var i = path.length - 1; i >= 0; i--) {
165 this.fireEventListeners_(path[i], event);
166 if (privates(event).impl.propagationStopped)
172 dispatchEventAtTargeting_: function(event) {
173 privates(event).impl.eventPhase = Event.AT_TARGET;
174 this.fireEventListeners_(this.wrapper, event);
175 return !privates(event).impl.propagationStopped;
178 dispatchEventAtBubbling_: function(event, path) {
179 privates(event).impl.eventPhase = Event.BUBBLING_PHASE;
180 for (var i = 0; i < path.length; i++) {
181 this.fireEventListeners_(path[i], event);
182 if (privates(event).impl.propagationStopped)
188 fireEventListeners_: function(node, event) {
189 var nodeImpl = privates(node).impl;
190 var listeners = nodeImpl.listeners[event.type];
193 var eventPhase = event.eventPhase;
194 for (var i = 0; i < listeners.length; i++) {
195 if (eventPhase == Event.CAPTURING_PHASE && !listeners[i].capture)
197 if (eventPhase == Event.BUBBLING_PHASE && listeners[i].capture)
201 listeners[i].callback(event);
203 console.error('Error in event handler for ' + event.type +
204 'during phase ' + eventPhase + ': ' +
205 e.message + '\nStack trace: ' + e.stack);
210 performAction_: function(actionType, opt_args) {
211 // Not yet initialized.
212 if (this.rootImpl.treeID === undefined ||
213 this.id === undefined) {
217 // Check permissions.
218 if (!IsInteractPermitted()) {
219 throw new Error(actionType + ' requires {"desktop": true} or' +
220 ' {"interact": true} in the "automation" manifest key.');
223 automationInternal.performAction({ treeID: this.rootImpl.treeID,
224 automationNodeID: this.id,
225 actionType: actionType },
229 querySelectorCallback_: function(userCallback, resultAutomationNodeID) {
230 // resultAutomationNodeID could be zero or undefined or (unlikely) null;
231 // they all amount to the same thing here, which is that no node was
233 if (!resultAutomationNodeID) {
237 var resultNode = this.rootImpl.get(resultAutomationNodeID);
239 logging.WARNING('Query selector result not in tree: ' +
240 resultAutomationNodeID);
243 userCallback(resultNode);
247 // Maps an attribute to its default value in an invalidated node.
248 // These attributes are taken directly from the Automation idl.
249 var AutomationAttributeDefaults = {
253 'location': { left: 0, top: 0, width: 0, height: 0 }
257 var AutomationAttributeTypes = [
268 * Maps an attribute name to another attribute who's value is an id or an array
269 * of ids referencing an AutomationNode.
270 * @param {!Object.<string, string>}
273 var ATTRIBUTE_NAME_TO_ATTRIBUTE_ID = {
274 'aria-activedescendant': 'activedescendantId',
275 'aria-controls': 'controlsIds',
276 'aria-describedby': 'describedbyIds',
277 'aria-flowto': 'flowtoIds',
278 'aria-labelledby': 'labelledbyIds',
279 'aria-owns': 'ownsIds'
283 * A set of attributes ignored in the automation API.
284 * @param {!Object.<string, boolean>}
287 var ATTRIBUTE_BLACKLIST = {'activedescendantId': true,
290 'describedbyIds': true,
292 'labelledbyIds': true,
298 * AutomationRootNode.
300 * An AutomationRootNode is the javascript end of an AXTree living in the
301 * browser. AutomationRootNode handles unserializing incremental updates from
302 * the source AXTree. Each update contains node data that form a complete tree
303 * after applying the update.
305 * A brief note about ids used through this class. The source AXTree assigns
306 * unique ids per node and we use these ids to build a hash to the actual
307 * AutomationNode object.
308 * Thus, tree traversals amount to a lookup in our hash.
310 * The tree itself is identified by the accessibility tree id of the
311 * renderer widget host.
314 function AutomationRootNodeImpl(treeID) {
315 AutomationNodeImpl.call(this, this);
316 this.treeID = treeID;
317 this.axNodeDataCache_ = {};
320 AutomationRootNodeImpl.prototype = {
321 __proto__: AutomationNodeImpl.prototype,
329 return this.axNodeDataCache_[id];
332 unserialize: function(update) {
333 var updateState = { pendingNodes: {}, newNodes: {} };
334 var oldRootId = this.id;
336 if (update.nodeIdToClear < 0) {
337 logging.WARNING('Bad nodeIdToClear: ' + update.nodeIdToClear);
338 lastError.set('automation',
339 'Bad update received on automation tree',
343 } else if (update.nodeIdToClear > 0) {
344 var nodeToClear = this.axNodeDataCache_[update.nodeIdToClear];
346 logging.WARNING('Bad nodeIdToClear: ' + update.nodeIdToClear +
348 lastError.set('automation',
349 'Bad update received on automation tree',
354 if (nodeToClear === this.wrapper) {
355 this.invalidate_(nodeToClear);
357 var children = nodeToClear.children();
358 for (var i = 0; i < children.length; i++)
359 this.invalidate_(children[i]);
360 var nodeToClearImpl = privates(nodeToClear).impl;
361 nodeToClearImpl.childIds = []
362 updateState.pendingNodes[nodeToClearImpl.id] = nodeToClear;
366 for (var i = 0; i < update.nodes.length; i++) {
367 if (!this.updateNode_(update.nodes[i], updateState))
371 if (Object.keys(updateState.pendingNodes).length > 0) {
372 logging.WARNING('Nodes left pending by the update: ' +
373 $JSON.stringify(updateState.pendingNodes));
374 lastError.set('automation',
375 'Bad update received on automation tree',
383 destroy: function() {
385 this.hostTree.childTree = undefined;
386 this.hostTree = undefined;
388 this.dispatchEvent(schema.EventType.destroyed);
389 this.invalidate_(this.wrapper);
392 onAccessibilityEvent: function(eventParams) {
393 if (!this.unserialize(eventParams.update)) {
394 logging.WARNING('unserialization failed');
398 var targetNode = this.get(eventParams.targetID);
400 var targetNodeImpl = privates(targetNode).impl;
401 targetNodeImpl.dispatchEvent(eventParams.eventType);
403 logging.WARNING('Got ' + eventParams.eventType +
404 ' event on unknown node: ' + eventParams.targetID +
405 '; this: ' + this.id);
410 toString: function() {
411 function toStringInternal(node, indent) {
415 new Array(indent).join(' ') +
416 AutomationNodeImpl.prototype.toString.call(node) +
419 for (var i = 0; i < node.children().length; i++)
420 output += toStringInternal(node.children()[i], indent);
423 return toStringInternal(this, 0);
426 invalidate_: function(node) {
429 var children = node.children();
431 for (var i = 0, child; child = children[i]; i++)
432 this.invalidate_(child);
434 // Retrieve the internal AutomationNodeImpl instance for this node.
435 // This object is not accessible outside of bindings code, but we can access
437 var nodeImpl = privates(node).impl;
438 var id = nodeImpl.id;
439 for (var key in AutomationAttributeDefaults) {
440 nodeImpl[key] = AutomationAttributeDefaults[key];
442 nodeImpl.childIds = [];
444 delete this.axNodeDataCache_[id];
447 deleteOldChildren_: function(node, newChildIds) {
448 // Create a set of child ids in |src| for fast lookup, and return false
449 // if a duplicate is found;
450 var newChildIdSet = {};
451 for (var i = 0; i < newChildIds.length; i++) {
452 var childId = newChildIds[i];
453 if (newChildIdSet[childId]) {
454 logging.WARNING('Node ' + privates(node).impl.id +
455 ' has duplicate child id ' + childId);
456 lastError.set('automation',
457 'Bad update received on automation tree',
462 newChildIdSet[newChildIds[i]] = true;
465 // Delete the old children.
466 var nodeImpl = privates(node).impl;
467 var oldChildIds = nodeImpl.childIds;
468 for (var i = 0; i < oldChildIds.length;) {
469 var oldId = oldChildIds[i];
470 if (!newChildIdSet[oldId]) {
471 this.invalidate_(this.axNodeDataCache_[oldId]);
472 oldChildIds.splice(i, 1);
477 nodeImpl.childIds = oldChildIds;
482 createNewChildren_: function(node, newChildIds, updateState) {
486 for (var i = 0; i < newChildIds.length; i++) {
487 var childId = newChildIds[i];
488 var childNode = this.axNodeDataCache_[childId];
490 if (childNode.parent() != node) {
492 if (childNode.parent()) {
493 var parentImpl = privates(childNode.parent()).impl;
494 parentId = parentImpl.id;
496 // This is a serious error - nodes should never be reparented.
497 // If this case occurs, continue so this node isn't left in an
498 // inconsistent state, but return failure at the end.
499 logging.WARNING('Node ' + childId + ' reparented from ' +
500 parentId + ' to ' + privates(node).impl.id);
501 lastError.set('automation',
502 'Bad update received on automation tree',
509 childNode = new AutomationNode(this);
510 this.axNodeDataCache_[childId] = childNode;
511 privates(childNode).impl.id = childId;
512 updateState.pendingNodes[childId] = childNode;
513 updateState.newNodes[childId] = childNode;
515 privates(childNode).impl.indexInParent = i;
516 privates(childNode).impl.parentID = privates(node).impl.id;
522 setData_: function(node, nodeData) {
523 var nodeImpl = privates(node).impl;
525 // TODO(dtseng): Make into set listing all hosting node roles.
526 if (nodeData.role == schema.RoleType.webView) {
527 if (nodeImpl.pendingChildFrame === undefined)
528 nodeImpl.pendingChildFrame = true;
530 if (nodeImpl.pendingChildFrame) {
531 nodeImpl.childTreeID = nodeData.intAttributes.childTreeId;
532 automationInternal.enableFrame(nodeImpl.childTreeID);
533 automationUtil.storeTreeCallback(nodeImpl.childTreeID, function(root) {
534 nodeImpl.pendingChildFrame = false;
535 nodeImpl.childTree = root;
536 privates(root).impl.hostTree = node;
537 nodeImpl.dispatchEvent(schema.EventType.childrenChanged);
541 for (var key in AutomationAttributeDefaults) {
543 nodeImpl[key] = nodeData[key];
545 nodeImpl[key] = AutomationAttributeDefaults[key];
547 for (var i = 0; i < AutomationAttributeTypes.length; i++) {
548 var attributeType = AutomationAttributeTypes[i];
549 for (var attributeName in nodeData[attributeType]) {
550 nodeImpl.attributesInternal[attributeName] =
551 nodeData[attributeType][attributeName];
552 if (ATTRIBUTE_BLACKLIST.hasOwnProperty(attributeName) ||
553 nodeImpl.attributes.hasOwnProperty(attributeName)) {
556 ATTRIBUTE_NAME_TO_ATTRIBUTE_ID.hasOwnProperty(attributeName)) {
557 this.defineReadonlyAttribute_(nodeImpl,
561 this.defineReadonlyAttribute_(nodeImpl,
568 defineReadonlyAttribute_: function(node, attributeName, opt_isIDRef) {
569 $Object.defineProperty(node.attributes, attributeName, {
573 var attributeId = node.attributesInternal[
574 ATTRIBUTE_NAME_TO_ATTRIBUTE_ID[attributeName]];
575 if (Array.isArray(attributeId)) {
576 return attributeId.map(function(current) {
577 return node.rootImpl.get(current);
580 return node.rootImpl.get(attributeId);
582 return node.attributesInternal[attributeName];
587 updateNode_: function(nodeData, updateState) {
588 var node = this.axNodeDataCache_[nodeData.id];
589 var didUpdateRoot = false;
591 delete updateState.pendingNodes[privates(node).impl.id];
593 if (nodeData.role != schema.RoleType.rootWebArea &&
594 nodeData.role != schema.RoleType.desktop) {
595 logging.WARNING(String(nodeData.id) +
596 ' is not in the cache and not the new root.');
597 lastError.set('automation',
598 'Bad update received on automation tree',
603 // |this| is an AutomationRootNodeImpl; retrieve the
604 // AutomationRootNode instance instead.
606 didUpdateRoot = true;
607 updateState.newNodes[this.id] = this.wrapper;
609 this.setData_(node, nodeData);
611 // TODO(aboxhall): send onChanged event?
613 if (!this.deleteOldChildren_(node, nodeData.childIds)) {
615 this.invalidate_(this.wrapper);
619 var nodeImpl = privates(node).impl;
621 var success = this.createNewChildren_(node,
624 nodeImpl.childIds = nodeData.childIds;
625 this.axNodeDataCache_[nodeImpl.id] = node;
632 var AutomationNode = utils.expose('AutomationNode',
634 { functions: ['parent',
645 'removeEventListener',
648 readonly: ['isRootNode',
656 var AutomationRootNode = utils.expose('AutomationRootNode',
657 AutomationRootNodeImpl,
658 { superclass: AutomationNode });
660 exports.AutomationNode = AutomationNode;
661 exports.AutomationRootNode = AutomationRootNode;