Explicitly add python-numpy dependency to install-build-deps.
[chromium-blink-merge.git] / chrome / renderer / resources / extensions / automation / automation_node.js
blobedbeaf14af70b30aad32da6771cfefe24f6492aa
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');
16 /**
17 * A single node in the Automation tree.
18 * @param {AutomationRootNodeImpl} root The root of the tree.
19 * @constructor
21 function AutomationNodeImpl(root) {
22 this.rootImpl = root;
23 this.childIds = [];
24 // Public attributes. No actual data gets set on this object.
25 this.attributes = {};
26 // Internal object holding all attributes.
27 this.attributesInternal = {};
28 this.listeners = {};
29 this.location = { left: 0, top: 0, width: 0, height: 0 };
32 AutomationNodeImpl.prototype = {
33 id: -1,
34 role: '',
35 state: { busy: true },
36 isRootNode: false,
38 get root() {
39 return this.rootImpl.wrapper;
42 parent: function() {
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() {
56 var children = [];
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));
61 return children;
64 previousSibling: function() {
65 var parent = this.parent();
66 if (parent && this.indexInParent > 0)
67 return parent.children()[this.indexInParent - 1];
68 return undefined;
71 nextSibling: function() {
72 var parent = this.parent();
73 if (parent && this.indexInParent < parent.children().length)
74 return parent.children()[this.indexInParent + 1];
75 return undefined;
78 doDefault: function() {
79 this.performAction_('doDefault');
82 focus: function() {
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);
134 toJSON: function() {
135 return { treeID: this.treeID,
136 id: this.id,
137 role: this.role,
138 attributes: this.attributes };
141 dispatchEvent: function(eventType) {
142 var path = [];
143 var parent = this.parent();
144 while (parent) {
145 path.push(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;
164 if (!impl)
165 impl = this;
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)
179 return false;
181 return true;
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)
195 return false;
197 return true;
200 fireEventListeners_: function(node, event) {
201 var nodeImpl = privates(node).impl;
202 var listeners = nodeImpl.listeners[event.type];
203 if (!listeners)
204 return;
205 var eventPhase = event.eventPhase;
206 for (var i = 0; i < listeners.length; i++) {
207 if (eventPhase == Event.CAPTURING_PHASE && !listeners[i].capture)
208 continue;
209 if (eventPhase == Event.BUBBLING_PHASE && listeners[i].capture)
210 continue;
212 try {
213 listeners[i].callback(event);
214 } catch (e) {
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) {
226 return;
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 },
238 opt_args || {});
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
244 // returned.
245 if (!resultAutomationNodeID) {
246 userCallback(null);
247 return;
249 var resultNode = this.rootImpl.get(resultAutomationNodeID);
250 if (!resultNode) {
251 logging.WARNING('Query selector result not in tree: ' +
252 resultAutomationNodeID);
253 userCallback(null);
255 userCallback(resultNode);
258 findInternal_: function(params, opt_results) {
259 var result = null;
260 this.forAllDescendants_(function(node) {
261 if (privates(node).impl.matchInternal_(params)) {
262 if (opt_results)
263 opt_results.push(node);
264 else
265 result = node;
266 return !opt_results;
269 if (opt_results)
270 return opt_results;
271 return result;
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();
284 if (closure(node))
285 return;
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)
295 return false;
297 if ('role' in params && this.role != params.role)
298 return false;
300 if ('state' in params) {
301 for (var state in params.state) {
302 if (params.state[state] != (state in this.state))
303 return false;
306 if ('attributes' in params) {
307 for (var attribute in params.attributes) {
308 if (!(attribute in this.attributesInternal))
309 return false;
311 var attrValue = params.attributes[attribute];
312 if (typeof attrValue != 'object') {
313 if (this.attributesInternal[attribute] !== attrValue)
314 return false;
315 } else if (attrValue instanceof RegExp) {
316 if (typeof this.attributesInternal[attribute] != 'string')
317 return false;
318 if (!attrValue.test(this.attributesInternal[attribute]))
319 return false;
320 } else {
321 // TODO(aboxhall): handle intlist case.
322 return false;
326 return true;
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 = {
333 'id': -1,
334 'role': '',
335 'state': {},
336 'location': { left: 0, top: 0, width: 0, height: 0 }
340 var AutomationAttributeTypes = [
341 'boolAttributes',
342 'floatAttributes',
343 'htmlAttributes',
344 'intAttributes',
345 'intlistAttributes',
346 'stringAttributes'
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>}
354 * @const
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>}
368 * @const
370 var ATTRIBUTE_BLACKLIST = {'activedescendantId': true,
371 'childTreeId': true,
372 'controlsIds': true,
373 'describedbyIds': true,
374 'flowtoIds': true,
375 'labelledbyIds': true,
376 'ownsIds': 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.
395 * @constructor
397 function AutomationRootNodeImpl(treeID) {
398 AutomationNodeImpl.call(this, this);
399 this.treeID = treeID;
400 this.axNodeDataCache_ = {};
403 AutomationRootNodeImpl.prototype = {
404 __proto__: AutomationNodeImpl.prototype,
406 isRootNode: true,
408 get: function(id) {
409 if (id == undefined)
410 return undefined;
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',
423 null,
424 chrome);
425 return false;
426 } else if (update.nodeIdToClear > 0) {
427 var nodeToClear = this.axNodeDataCache_[update.nodeIdToClear];
428 if (!nodeToClear) {
429 logging.WARNING('Bad nodeIdToClear: ' + update.nodeIdToClear +
430 ' (not in cache)');
431 lastError.set('automation',
432 'Bad update received on automation tree',
433 null,
434 chrome);
435 return false;
437 if (nodeToClear === this.wrapper) {
438 this.invalidate_(nodeToClear);
439 } else {
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))
451 return false;
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',
459 null,
460 chrome);
461 return false;
463 return true;
466 destroy: function() {
467 if (this.hostTree)
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');
478 return false;
481 var targetNode = this.get(eventParams.targetID);
482 if (targetNode) {
483 var targetNodeImpl = privates(targetNode).impl;
484 targetNodeImpl.dispatchEvent(eventParams.eventType);
485 } else {
486 logging.WARNING('Got ' + eventParams.eventType +
487 ' event on unknown node: ' + eventParams.targetID +
488 '; this: ' + this.id);
490 return true;
493 toString: function() {
494 function toStringInternal(node, indent) {
495 if (!node)
496 return '';
497 var output =
498 new Array(indent).join(' ') +
499 AutomationNodeImpl.prototype.toString.call(node) +
500 '\n';
501 indent += 2;
502 for (var i = 0; i < node.children().length; i++)
503 output += toStringInternal(node.children()[i], indent);
504 return output;
506 return toStringInternal(this, 0);
509 invalidate_: function(node) {
510 if (!node)
511 return;
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
519 // it here.
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 = [];
526 nodeImpl.id = id;
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',
541 null,
542 chrome);
543 return false;
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);
556 } else {
557 i++;
560 nodeImpl.childIds = oldChildIds;
562 return true;
565 createNewChildren_: function(node, newChildIds, updateState) {
566 logging.CHECK(node);
567 var success = true;
569 for (var i = 0; i < newChildIds.length; i++) {
570 var childId = newChildIds[i];
571 var childNode = this.axNodeDataCache_[childId];
572 if (childNode) {
573 if (childNode.parent() != node) {
574 var parentId = -1;
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',
586 null,
587 chrome);
588 success = false;
589 continue;
591 } else {
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;
602 return success;
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) {
625 if (key in nodeData)
626 nodeImpl[key] = nodeData[key];
627 else
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)) {
637 continue;
638 } else if (
639 ATTRIBUTE_NAME_TO_ATTRIBUTE_ID.hasOwnProperty(attributeName)) {
640 this.defineReadonlyAttribute_(nodeImpl,
641 attributeName,
642 true);
643 } else {
644 this.defineReadonlyAttribute_(nodeImpl,
645 attributeName);
651 defineReadonlyAttribute_: function(node, attributeName, opt_isIDRef) {
652 $Object.defineProperty(node.attributes, attributeName, {
653 enumerable: true,
654 get: function() {
655 if (opt_isIDRef) {
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);
661 }, this);
663 return node.rootImpl.get(attributeId);
665 return node.attributesInternal[attributeName];
666 }.bind(this),
670 updateNode_: function(nodeData, updateState) {
671 var node = this.axNodeDataCache_[nodeData.id];
672 var didUpdateRoot = false;
673 if (node) {
674 delete updateState.pendingNodes[privates(node).impl.id];
675 } else {
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',
682 null,
683 chrome);
684 return false;
686 // |this| is an AutomationRootNodeImpl; retrieve the
687 // AutomationRootNode instance instead.
688 node = this.wrapper;
689 didUpdateRoot = true;
690 updateState.newNodes[this.id] = this.wrapper;
692 this.setData_(node, nodeData);
694 // TODO(aboxhall): send onChanged event?
695 logging.CHECK(node);
696 if (!this.deleteOldChildren_(node, nodeData.childIds)) {
697 if (didUpdateRoot) {
698 this.invalidate_(this.wrapper);
700 return false;
702 var nodeImpl = privates(node).impl;
704 var success = this.createNewChildren_(node,
705 nodeData.childIds,
706 updateState);
707 nodeImpl.childIds = nodeData.childIds;
708 this.axNodeDataCache_[nodeImpl.id] = node;
710 return success;
715 var AutomationNode = utils.expose('AutomationNode',
716 AutomationNodeImpl,
717 { functions: ['parent',
718 'firstChild',
719 'lastChild',
720 'children',
721 'previousSibling',
722 'nextSibling',
723 'doDefault',
724 'find',
725 'findAll',
726 'focus',
727 'makeVisible',
728 'matches',
729 'setSelection',
730 'addEventListener',
731 'removeEventListener',
732 'domQuerySelector',
733 'toJSON'],
734 readonly: ['isRootNode',
735 'role',
736 'state',
737 'location',
738 'attributes',
739 'indexInParent',
740 'root'] });
742 var AutomationRootNode = utils.expose('AutomationRootNode',
743 AutomationRootNodeImpl,
744 { superclass: AutomationNode });
746 exports.AutomationNode = AutomationNode;
747 exports.AutomationRootNode = AutomationRootNode;