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.rootImpl.get(this.parentID);
46 firstChild: function() {
47 var node = this.rootImpl.get(this.childIds[0]);
51 lastChild: function() {
52 var childIds = this.childIds;
53 var node = this.rootImpl.get(childIds[childIds.length - 1]);
57 children: function() {
59 for (var i = 0, childID; childID = this.childIds[i]; i++) {
60 logging.CHECK(this.rootImpl.get(childID));
61 children.push(this.rootImpl.get(childID));
66 previousSibling: function() {
67 var parent = this.parent();
68 if (parent && this.indexInParent > 0)
69 return parent.children()[this.indexInParent - 1];
73 nextSibling: function() {
74 var parent = this.parent();
75 if (parent && this.indexInParent < parent.children().length)
76 return parent.children()[this.indexInParent + 1];
80 doDefault: function() {
81 this.performAction_('doDefault');
85 this.performAction_('focus');
88 makeVisible: function() {
89 this.performAction_('makeVisible');
92 setSelection: function(startIndex, endIndex) {
93 this.performAction_('setSelection',
94 { startIndex: startIndex,
95 endIndex: endIndex });
98 addEventListener: function(eventType, callback, capture) {
99 this.removeEventListener(eventType, callback);
100 if (!this.listeners[eventType])
101 this.listeners[eventType] = [];
102 this.listeners[eventType].push({callback: callback, capture: !!capture});
105 // TODO(dtseng/aboxhall): Check this impl against spec.
106 removeEventListener: function(eventType, callback) {
107 if (this.listeners[eventType]) {
108 var listeners = this.listeners[eventType];
109 for (var i = 0; i < listeners.length; i++) {
110 if (callback === listeners[i].callback)
111 listeners.splice(i, 1);
116 dispatchEvent: function(eventType) {
118 var parent = this.parent();
121 // TODO(aboxhall/dtseng): handle unloaded parent node
122 parent = parent.parent();
124 var event = new AutomationEvent(eventType, this.wrapper);
126 // Dispatch the event through the propagation path in three phases:
127 // - capturing: starting from the root and going down to the target's parent
128 // - targeting: dispatching the event on the target itself
129 // - bubbling: starting from the target's parent, going back up to the root.
130 // At any stage, a listener may call stopPropagation() on the event, which
131 // will immediately stop event propagation through this path.
132 if (this.dispatchEventAtCapturing_(event, path)) {
133 if (this.dispatchEventAtTargeting_(event, path))
134 this.dispatchEventAtBubbling_(event, path);
138 toString: function() {
139 return 'node id=' + this.id +
140 ' role=' + this.role +
141 ' state=' + $JSON.stringify(this.state) +
142 ' parentID=' + this.parentID +
143 ' childIds=' + $JSON.stringify(this.childIds) +
144 ' attributes=' + $JSON.stringify(this.attributes);
147 dispatchEventAtCapturing_: function(event, path) {
148 privates(event).impl.eventPhase = Event.CAPTURING_PHASE;
149 for (var i = path.length - 1; i >= 0; i--) {
150 this.fireEventListeners_(path[i], event);
151 if (privates(event).impl.propagationStopped)
157 dispatchEventAtTargeting_: function(event) {
158 privates(event).impl.eventPhase = Event.AT_TARGET;
159 this.fireEventListeners_(this.wrapper, event);
160 return !privates(event).impl.propagationStopped;
163 dispatchEventAtBubbling_: function(event, path) {
164 privates(event).impl.eventPhase = Event.BUBBLING_PHASE;
165 for (var i = 0; i < path.length; i++) {
166 this.fireEventListeners_(path[i], event);
167 if (privates(event).impl.propagationStopped)
173 fireEventListeners_: function(node, event) {
174 var nodeImpl = privates(node).impl;
175 var listeners = nodeImpl.listeners[event.type];
178 var eventPhase = event.eventPhase;
179 for (var i = 0; i < listeners.length; i++) {
180 if (eventPhase == Event.CAPTURING_PHASE && !listeners[i].capture)
182 if (eventPhase == Event.BUBBLING_PHASE && listeners[i].capture)
186 listeners[i].callback(event);
188 console.error('Error in event handler for ' + event.type +
189 'during phase ' + eventPhase + ': ' +
190 e.message + '\nStack trace: ' + e.stack);
195 performAction_: function(actionType, opt_args) {
196 // Not yet initialized.
197 if (this.rootImpl.processID === undefined ||
198 this.rootImpl.routingID === undefined ||
199 this.id === undefined) {
203 // Check permissions.
204 if (!IsInteractPermitted()) {
205 throw new Error(actionType + ' requires {"desktop": true} or' +
206 ' {"interact": true} in the "automation" manifest key.');
209 automationInternal.performAction({ processID: this.rootImpl.processID,
210 routingID: this.rootImpl.routingID,
211 automationNodeID: this.id,
212 actionType: actionType },
217 // Maps an attribute to its default value in an invalidated node.
218 // These attributes are taken directly from the Automation idl.
219 var AutomationAttributeDefaults = {
223 'location': { left: 0, top: 0, width: 0, height: 0 }
227 var AutomationAttributeTypes = [
238 * Maps an attribute name to another attribute who's value is an id or an array
239 * of ids referencing an AutomationNode.
240 * @param {!Object.<string, string>}
243 var ATTRIBUTE_NAME_TO_ATTRIBUTE_ID = {
244 'aria-activedescendant': 'activedescendantId',
245 'aria-controls': 'controlsIds',
246 'aria-describedby': 'describedbyIds',
247 'aria-flowto': 'flowtoIds',
248 'aria-labelledby': 'labelledbyIds',
249 'aria-owns': 'ownsIds'
253 * A set of attributes ignored in the automation API.
254 * @param {!Object.<string, boolean>}
257 var ATTRIBUTE_BLACKLIST = {'activedescendantId': true,
259 'describedbyIds': true,
261 'labelledbyIds': true,
267 * AutomationRootNode.
269 * An AutomationRootNode is the javascript end of an AXTree living in the
270 * browser. AutomationRootNode handles unserializing incremental updates from
271 * the source AXTree. Each update contains node data that form a complete tree
272 * after applying the update.
274 * A brief note about ids used through this class. The source AXTree assigns
275 * unique ids per node and we use these ids to build a hash to the actual
276 * AutomationNode object.
277 * Thus, tree traversals amount to a lookup in our hash.
279 * The tree itself is identified by the process id and routing id of the
280 * renderer widget host.
283 function AutomationRootNodeImpl(processID, routingID) {
284 AutomationNodeImpl.call(this, this);
285 this.processID = processID;
286 this.routingID = routingID;
287 this.axNodeDataCache_ = {};
290 AutomationRootNodeImpl.prototype = {
291 __proto__: AutomationNodeImpl.prototype,
299 return this.axNodeDataCache_[id];
302 unserialize: function(update) {
303 var updateState = { pendingNodes: {}, newNodes: {} };
304 var oldRootId = this.id;
306 if (update.nodeIdToClear < 0) {
307 logging.WARNING('Bad nodeIdToClear: ' + update.nodeIdToClear);
308 lastError.set('automation',
309 'Bad update received on automation tree',
313 } else if (update.nodeIdToClear > 0) {
314 var nodeToClear = this.axNodeDataCache_[update.nodeIdToClear];
316 logging.WARNING('Bad nodeIdToClear: ' + update.nodeIdToClear +
318 lastError.set('automation',
319 'Bad update received on automation tree',
324 if (nodeToClear === this.wrapper) {
325 this.invalidate_(nodeToClear);
327 var children = nodeToClear.children();
328 for (var i = 0; i < children.length; i++)
329 this.invalidate_(children[i]);
330 var nodeToClearImpl = privates(nodeToClear).impl;
331 nodeToClearImpl.childIds = []
332 updateState.pendingNodes[nodeToClearImpl.id] = nodeToClear;
336 for (var i = 0; i < update.nodes.length; i++) {
337 if (!this.updateNode_(update.nodes[i], updateState))
341 if (Object.keys(updateState.pendingNodes).length > 0) {
342 logging.WARNING('Nodes left pending by the update: ' +
343 $JSON.stringify(updateState.pendingNodes));
344 lastError.set('automation',
345 'Bad update received on automation tree',
353 destroy: function() {
354 this.dispatchEvent(schema.EventType.destroyed);
355 this.invalidate_(this.wrapper);
358 onAccessibilityEvent: function(eventParams) {
359 if (!this.unserialize(eventParams.update)) {
360 logging.WARNING('unserialization failed');
364 var targetNode = this.get(eventParams.targetID);
366 var targetNodeImpl = privates(targetNode).impl;
367 targetNodeImpl.dispatchEvent(eventParams.eventType);
369 logging.WARNING('Got ' + eventParams.eventType +
370 ' event on unknown node: ' + eventParams.targetID +
371 '; this: ' + this.id);
376 toString: function() {
377 function toStringInternal(node, indent) {
381 new Array(indent).join(' ') +
382 AutomationNodeImpl.prototype.toString.call(node) +
385 for (var i = 0; i < node.children().length; i++)
386 output += toStringInternal(node.children()[i], indent);
389 return toStringInternal(this, 0);
392 invalidate_: function(node) {
395 var children = node.children();
397 for (var i = 0, child; child = children[i]; i++)
398 this.invalidate_(child);
400 // Retrieve the internal AutomationNodeImpl instance for this node.
401 // This object is not accessible outside of bindings code, but we can access
403 var nodeImpl = privates(node).impl;
404 var id = nodeImpl.id;
405 for (var key in AutomationAttributeDefaults) {
406 nodeImpl[key] = AutomationAttributeDefaults[key];
408 nodeImpl.childIds = [];
409 nodeImpl.loaded = false;
411 delete this.axNodeDataCache_[id];
414 load: function(callback) {
415 // TODO(dtseng/aboxhall): Implement.
417 throw 'Unsupported state: root node is not loaded.';
419 setTimeout(callback, 0);
422 deleteOldChildren_: function(node, newChildIds) {
423 // Create a set of child ids in |src| for fast lookup, and return false
424 // if a duplicate is found;
425 var newChildIdSet = {};
426 for (var i = 0; i < newChildIds.length; i++) {
427 var childId = newChildIds[i];
428 if (newChildIdSet[childId]) {
429 logging.WARNING('Node ' + privates(node).impl.id +
430 ' has duplicate child id ' + childId);
431 lastError.set('automation',
432 'Bad update received on automation tree',
437 newChildIdSet[newChildIds[i]] = true;
440 // Delete the old children.
441 var nodeImpl = privates(node).impl;
442 var oldChildIds = nodeImpl.childIds;
443 for (var i = 0; i < oldChildIds.length;) {
444 var oldId = oldChildIds[i];
445 if (!newChildIdSet[oldId]) {
446 this.invalidate_(this.axNodeDataCache_[oldId]);
447 oldChildIds.splice(i, 1);
452 nodeImpl.childIds = oldChildIds;
457 createNewChildren_: function(node, newChildIds, updateState) {
460 for (var i = 0; i < newChildIds.length; i++) {
461 var childId = newChildIds[i];
462 var childNode = this.axNodeDataCache_[childId];
464 if (childNode.parent() != node) {
466 if (childNode.parent()) {
467 var parentImpl = privates(childNode.parent()).impl;
468 parentId = parentImpl.id;
470 // This is a serious error - nodes should never be reparented.
471 // If this case occurs, continue so this node isn't left in an
472 // inconsistent state, but return failure at the end.
473 logging.WARNING('Node ' + childId + ' reparented from ' +
474 parentId + ' to ' + privates(node).impl.id);
475 lastError.set('automation',
476 'Bad update received on automation tree',
483 childNode = new AutomationNode(this);
484 this.axNodeDataCache_[childId] = childNode;
485 privates(childNode).impl.id = childId;
486 updateState.pendingNodes[childId] = childNode;
487 updateState.newNodes[childId] = childNode;
489 privates(childNode).impl.indexInParent = i;
490 privates(childNode).impl.parentID = privates(node).impl.id;
496 setData_: function(node, nodeData) {
497 var nodeImpl = privates(node).impl;
498 for (var key in AutomationAttributeDefaults) {
500 nodeImpl[key] = nodeData[key];
502 nodeImpl[key] = AutomationAttributeDefaults[key];
504 for (var i = 0; i < AutomationAttributeTypes.length; i++) {
505 var attributeType = AutomationAttributeTypes[i];
506 for (var attributeName in nodeData[attributeType]) {
507 nodeImpl.attributesInternal[attributeName] =
508 nodeData[attributeType][attributeName];
509 if (ATTRIBUTE_BLACKLIST.hasOwnProperty(attributeName) ||
510 nodeImpl.attributes.hasOwnProperty(attributeName)) {
513 ATTRIBUTE_NAME_TO_ATTRIBUTE_ID.hasOwnProperty(attributeName)) {
514 this.defineReadonlyAttribute_(nodeImpl,
518 this.defineReadonlyAttribute_(nodeImpl,
525 defineReadonlyAttribute_: function(node, attributeName, opt_isIDRef) {
526 $Object.defineProperty(node.attributes, attributeName, {
530 var attributeId = node.attributesInternal[
531 ATTRIBUTE_NAME_TO_ATTRIBUTE_ID[attributeName]];
532 if (Array.isArray(attributeId)) {
533 return attributeId.map(function(current) {
534 return node.rootImpl.get(current);
537 return node.rootImpl.get(attributeId);
539 return node.attributesInternal[attributeName];
544 updateNode_: function(nodeData, updateState) {
545 var node = this.axNodeDataCache_[nodeData.id];
546 var didUpdateRoot = false;
548 delete updateState.pendingNodes[privates(node).impl.id];
550 if (nodeData.role != schema.RoleType.rootWebArea &&
551 nodeData.role != schema.RoleType.desktop) {
552 logging.WARNING(String(nodeData.id) +
553 ' is not in the cache and not the new root.');
554 lastError.set('automation',
555 'Bad update received on automation tree',
560 // |this| is an AutomationRootNodeImpl; retrieve the
561 // AutomationRootNode instance instead.
563 didUpdateRoot = true;
564 updateState.newNodes[this.id] = this.wrapper;
566 this.setData_(node, nodeData);
568 // TODO(aboxhall): send onChanged event?
570 if (!this.deleteOldChildren_(node, nodeData.childIds)) {
572 this.invalidate_(this.wrapper);
576 var nodeImpl = privates(node).impl;
578 var success = this.createNewChildren_(node,
581 nodeImpl.childIds = nodeData.childIds;
582 this.axNodeDataCache_[nodeImpl.id] = node;
589 var AutomationNode = utils.expose('AutomationNode',
591 { functions: ['parent',
602 'removeEventListener'],
603 readonly: ['isRootNode',
610 var AutomationRootNode = utils.expose('AutomationRootNode',
611 AutomationRootNodeImpl,
612 { superclass: AutomationNode,
614 readonly: ['loaded'] });
616 exports.AutomationNode = AutomationNode;
617 exports.AutomationRootNode = AutomationRootNode;