Use multiline attribute to check for IA2_STATE_MULTILINE.
[chromium-blink-merge.git] / chrome / renderer / resources / extensions / automation / automation_node.js
blob4daf7e4a869b51f7aa855321e69b8c2cd59998c0
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
20  */
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;
40   },
42   get parent() {
43     return this.hostTree || this.rootImpl.get(this.parentID);
44   },
46   get firstChild() {
47     return this.childTree || this.rootImpl.get(this.childIds[0]);
48   },
50   get lastChild() {
51     var childIds = this.childIds;
52     return this.childTree || this.rootImpl.get(childIds[childIds.length - 1]);
53   },
55   get children() {
56     if (this.childTree)
57       return [this.childTree];
59     var children = [];
60     for (var i = 0, childID; childID = this.childIds[i]; i++) {
61       logging.CHECK(this.rootImpl.get(childID));
62       children.push(this.rootImpl.get(childID));
63     }
64     return children;
65   },
67   get previousSibling() {
68     var parent = this.parent;
69     if (parent && this.indexInParent > 0)
70       return parent.children[this.indexInParent - 1];
71     return undefined;
72   },
74   get nextSibling() {
75     var parent = this.parent;
76     if (parent && this.indexInParent < parent.children.length)
77       return parent.children[this.indexInParent + 1];
78     return undefined;
79   },
81   doDefault: function() {
82     this.performAction_('doDefault');
83   },
85   focus: function() {
86     this.performAction_('focus');
87   },
89   makeVisible: function() {
90     this.performAction_('makeVisible');
91   },
93   setSelection: function(startIndex, endIndex) {
94     this.performAction_('setSelection',
95                         { startIndex: startIndex,
96                           endIndex: endIndex });
97   },
99   domQuerySelector: function(selector, callback) {
100     automationInternal.querySelector(
101       { treeID: this.rootImpl.treeID,
102         automationNodeID: this.id,
103         selector: selector },
104       this.domQuerySelectorCallback_.bind(this, callback));
105   },
107   find: function(params) {
108     return this.findInternal_(params);
109   },
111   findAll: function(params) {
112     return this.findInternal_(params, []);
113   },
115   matches: function(params) {
116     return this.matchInternal_(params);
117   },
119   addEventListener: function(eventType, callback, capture) {
120     this.removeEventListener(eventType, callback);
121     if (!this.listeners[eventType])
122       this.listeners[eventType] = [];
123     this.listeners[eventType].push({callback: callback, capture: !!capture});
124   },
126   // TODO(dtseng/aboxhall): Check this impl against spec.
127   removeEventListener: function(eventType, callback) {
128     if (this.listeners[eventType]) {
129       var listeners = this.listeners[eventType];
130       for (var i = 0; i < listeners.length; i++) {
131         if (callback === listeners[i].callback)
132           listeners.splice(i, 1);
133       }
134     }
135   },
137   toJSON: function() {
138     return { treeID: this.treeID,
139              id: this.id,
140              role: this.role,
141              attributes: this.attributes };
142   },
144   dispatchEvent: function(eventType) {
145     var path = [];
146     var parent = this.parent;
147     while (parent) {
148       path.push(parent);
149       parent = parent.parent;
150     }
151     var event = new AutomationEvent(eventType, this.wrapper);
153     // Dispatch the event through the propagation path in three phases:
154     // - capturing: starting from the root and going down to the target's parent
155     // - targeting: dispatching the event on the target itself
156     // - bubbling: starting from the target's parent, going back up to the root.
157     // At any stage, a listener may call stopPropagation() on the event, which
158     // will immediately stop event propagation through this path.
159     if (this.dispatchEventAtCapturing_(event, path)) {
160       if (this.dispatchEventAtTargeting_(event, path))
161         this.dispatchEventAtBubbling_(event, path);
162     }
163   },
165   toString: function() {
166     var impl = privates(this).impl;
167     if (!impl)
168       impl = this;
169     return 'node id=' + impl.id +
170         ' role=' + this.role +
171         ' state=' + $JSON.stringify(this.state) +
172         ' parentID=' + impl.parentID +
173         ' childIds=' + $JSON.stringify(impl.childIds) +
174         ' attributes=' + $JSON.stringify(this.attributes);
175   },
177   dispatchEventAtCapturing_: function(event, path) {
178     privates(event).impl.eventPhase = Event.CAPTURING_PHASE;
179     for (var i = path.length - 1; i >= 0; i--) {
180       this.fireEventListeners_(path[i], event);
181       if (privates(event).impl.propagationStopped)
182         return false;
183     }
184     return true;
185   },
187   dispatchEventAtTargeting_: function(event) {
188     privates(event).impl.eventPhase = Event.AT_TARGET;
189     this.fireEventListeners_(this.wrapper, event);
190     return !privates(event).impl.propagationStopped;
191   },
193   dispatchEventAtBubbling_: function(event, path) {
194     privates(event).impl.eventPhase = Event.BUBBLING_PHASE;
195     for (var i = 0; i < path.length; i++) {
196       this.fireEventListeners_(path[i], event);
197       if (privates(event).impl.propagationStopped)
198         return false;
199     }
200     return true;
201   },
203   fireEventListeners_: function(node, event) {
204     var nodeImpl = privates(node).impl;
205     var listeners = nodeImpl.listeners[event.type];
206     if (!listeners)
207       return;
208     var eventPhase = event.eventPhase;
209     for (var i = 0; i < listeners.length; i++) {
210       if (eventPhase == Event.CAPTURING_PHASE && !listeners[i].capture)
211         continue;
212       if (eventPhase == Event.BUBBLING_PHASE && listeners[i].capture)
213         continue;
215       try {
216         listeners[i].callback(event);
217       } catch (e) {
218         console.error('Error in event handler for ' + event.type +
219                       'during phase ' + eventPhase + ': ' +
220                       e.message + '\nStack trace: ' + e.stack);
221       }
222     }
223   },
225   performAction_: function(actionType, opt_args) {
226     // Not yet initialized.
227     if (this.rootImpl.treeID === undefined ||
228         this.id === undefined) {
229       return;
230     }
232     // Check permissions.
233     if (!IsInteractPermitted()) {
234       throw new Error(actionType + ' requires {"desktop": true} or' +
235           ' {"interact": true} in the "automation" manifest key.');
236     }
238     automationInternal.performAction({ treeID: this.rootImpl.treeID,
239                                        automationNodeID: this.id,
240                                        actionType: actionType },
241                                      opt_args || {});
242   },
244   domQuerySelectorCallback_: function(userCallback, resultAutomationNodeID) {
245     // resultAutomationNodeID could be zero or undefined or (unlikely) null;
246     // they all amount to the same thing here, which is that no node was
247     // returned.
248     if (!resultAutomationNodeID) {
249       userCallback(null);
250       return;
251     }
252     var resultNode = this.rootImpl.get(resultAutomationNodeID);
253     if (!resultNode) {
254       logging.WARNING('Query selector result not in tree: ' +
255                       resultAutomationNodeID);
256       userCallback(null);
257     }
258     userCallback(resultNode);
259   },
261   findInternal_: function(params, opt_results) {
262     var result = null;
263     this.forAllDescendants_(function(node) {
264       if (privates(node).impl.matchInternal_(params)) {
265         if (opt_results)
266           opt_results.push(node);
267         else
268           result = node;
269         return !opt_results;
270       }
271     });
272     if (opt_results)
273       return opt_results;
274     return result;
275   },
277   /**
278    * Executes a closure for all of this node's descendants, in pre-order.
279    * Early-outs if the closure returns true.
280    * @param {Function(AutomationNode):boolean} closure Closure to be executed
281    *     for each node. Return true to early-out the traversal.
282    */
283   forAllDescendants_: function(closure) {
284     var stack = this.wrapper.children.reverse();
285     while (stack.length > 0) {
286       var node = stack.pop();
287       if (closure(node))
288         return;
290       var children = node.children;
291       for (var i = children.length - 1; i >= 0; i--)
292         stack.push(children[i]);
293     }
294   },
296   matchInternal_: function(params) {
297     if (Object.keys(params).length == 0)
298       return false;
300     if ('role' in params && this.role != params.role)
301         return false;
303     if ('state' in params) {
304       for (var state in params.state) {
305         if (params.state[state] != (state in this.state))
306           return false;
307       }
308     }
309     if ('attributes' in params) {
310       for (var attribute in params.attributes) {
311         if (!(attribute in this.attributesInternal))
312           return false;
314         var attrValue = params.attributes[attribute];
315         if (typeof attrValue != 'object') {
316           if (this.attributesInternal[attribute] !== attrValue)
317             return false;
318         } else if (attrValue instanceof RegExp) {
319           if (typeof this.attributesInternal[attribute] != 'string')
320             return false;
321           if (!attrValue.test(this.attributesInternal[attribute]))
322             return false;
323         } else {
324           // TODO(aboxhall): handle intlist case.
325           return false;
326         }
327       }
328     }
329     return true;
330   }
333 // Maps an attribute to its default value in an invalidated node.
334 // These attributes are taken directly from the Automation idl.
335 var AutomationAttributeDefaults = {
336   'id': -1,
337   'role': '',
338   'state': {},
339   'location': { left: 0, top: 0, width: 0, height: 0 }
343 var AutomationAttributeTypes = [
344   'boolAttributes',
345   'floatAttributes',
346   'htmlAttributes',
347   'intAttributes',
348   'intlistAttributes',
349   'stringAttributes'
353  * Maps an attribute name to another attribute who's value is an id or an array
354  * of ids referencing an AutomationNode.
355  * @param {!Object<string, string>}
356  * @const
357  */
358 var ATTRIBUTE_NAME_TO_ID_ATTRIBUTE = {
359   'aria-activedescendant': 'activedescendantId',
360   'aria-controls': 'controlsIds',
361   'aria-describedby': 'describedbyIds',
362   'aria-flowto': 'flowtoIds',
363   'aria-labelledby': 'labelledbyIds',
364   'aria-owns': 'ownsIds'
368  * A set of attributes ignored in the automation API.
369  * @param {!Object<string, boolean>}
370  * @const
371  */
372 var ATTRIBUTE_BLACKLIST = {'activedescendantId': true,
373                            'childTreeId': true,
374                            'controlsIds': true,
375                            'describedbyIds': true,
376                            'flowtoIds': true,
377                            'labelledbyIds': true,
378                            'ownsIds': true
381 function defaultStringAttribute(opt_defaultVal) {
382   var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : '';
383   return { default: defaultVal, reflectFrom: 'stringAttributes' };
386 function defaultIntAttribute(opt_defaultVal) {
387   var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : 0;
388   return { default: defaultVal, reflectFrom: 'intAttributes' };
391 function defaultFloatAttribute(opt_defaultVal) {
392   var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : 0;
393   return { default: defaultVal, reflectFrom: 'floatAttributes' };
396 function defaultBoolAttribute(opt_defaultVal) {
397   var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : false;
398   return { default: defaultVal, reflectFrom: 'boolAttributes' };
401 function defaultHtmlAttribute(opt_defaultVal) {
402   var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : '';
403   return { default: defaultVal, reflectFrom: 'htmlAttributes' };
406 function defaultIntListAttribute(opt_defaultVal) {
407   var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : [];
408   return { default: defaultVal, reflectFrom: 'intlistAttributes' };
411 function defaultNodeRefAttribute(idAttribute, opt_defaultVal) {
412   var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : null;
413   return { default: defaultVal,
414            idFrom: 'intAttributes',
415            idAttribute: idAttribute,
416            isRef: true };
419 function defaultNodeRefListAttribute(idAttribute, opt_defaultVal) {
420   var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : [];
421   return { default: [],
422            idFrom: 'intlistAttributes',
423            idAttribute: idAttribute,
424            isRef: true };
427 // Maps an attribute to its default value in an invalidated node.
428 // These attributes are taken directly from the Automation idl.
429 var DefaultMixinAttributes = {
430   description: defaultStringAttribute(),
431   help: defaultStringAttribute(),
432   name: defaultStringAttribute(),
433   value: defaultStringAttribute(),
434   controls: defaultNodeRefListAttribute('controlsIds'),
435   describedby: defaultNodeRefListAttribute('describedbyIds'),
436   flowto: defaultNodeRefListAttribute('flowtoIds'),
437   labelledby: defaultNodeRefListAttribute('labelledbyIds'),
438   owns: defaultNodeRefListAttribute('ownsIds')
441 var ActiveDescendantMixinAttribute = {
442   activedescendant: defaultNodeRefAttribute('activedescendantId')
445 var LinkMixinAttributes = {
446   url: defaultStringAttribute()
449 var DocumentMixinAttributes = {
450   docUrl: defaultStringAttribute(),
451   docTitle: defaultStringAttribute(),
452   docLoaded: defaultStringAttribute(),
453   docLoadingProgress: defaultFloatAttribute()
456 var ScrollableMixinAttributes = {
457   scrollX: defaultIntAttribute(),
458   scrollXMin: defaultIntAttribute(),
459   scrollXMax: defaultIntAttribute(),
460   scrollY: defaultIntAttribute(),
461   scrollYMin: defaultIntAttribute(),
462   scrollYMax: defaultIntAttribute()
465 var EditableTextMixinAttributes = {
466   textSelStart: defaultIntAttribute(-1),
467   textSelEnd: defaultIntAttribute(-1)
470 var RangeMixinAttributes = {
471   valueForRange: defaultFloatAttribute(),
472   minValueForRange: defaultFloatAttribute(),
473   maxValueForRange: defaultFloatAttribute()
476 var TableMixinAttributes = {
477   tableRowCount: defaultIntAttribute(),
478   tableColumnCount: defaultIntAttribute()
481 var TableCellMixinAttributes = {
482   tableCellColumnIndex: defaultIntAttribute(),
483   tableCellColumnSpan: defaultIntAttribute(1),
484   tableCellRowIndex: defaultIntAttribute(),
485   tableCellRowSpan: defaultIntAttribute(1)
488 var LiveRegionMixinAttributes = {
489   containerLiveAtomic: defaultBoolAttribute(),
490   containerLiveBusy: defaultBoolAttribute(),
491   containerLiveRelevant: defaultStringAttribute(),
492   containerLiveStatus: defaultStringAttribute(),
496  * AutomationRootNode.
498  * An AutomationRootNode is the javascript end of an AXTree living in the
499  * browser. AutomationRootNode handles unserializing incremental updates from
500  * the source AXTree. Each update contains node data that form a complete tree
501  * after applying the update.
503  * A brief note about ids used through this class. The source AXTree assigns
504  * unique ids per node and we use these ids to build a hash to the actual
505  * AutomationNode object.
506  * Thus, tree traversals amount to a lookup in our hash.
508  * The tree itself is identified by the accessibility tree id of the
509  * renderer widget host.
510  * @constructor
511  */
512 function AutomationRootNodeImpl(treeID) {
513   AutomationNodeImpl.call(this, this);
514   this.treeID = treeID;
515   this.axNodeDataCache_ = {};
518 AutomationRootNodeImpl.prototype = {
519   __proto__: AutomationNodeImpl.prototype,
521   isRootNode: true,
522   treeID: -1,
524   get: function(id) {
525     if (id == undefined)
526       return undefined;
528     return this.axNodeDataCache_[id];
529   },
531   unserialize: function(update) {
532     var updateState = { pendingNodes: {}, newNodes: {} };
533     var oldRootId = this.id;
535     if (update.nodeIdToClear < 0) {
536         logging.WARNING('Bad nodeIdToClear: ' + update.nodeIdToClear);
537         lastError.set('automation',
538                       'Bad update received on automation tree',
539                       null,
540                       chrome);
541         return false;
542     } else if (update.nodeIdToClear > 0) {
543       var nodeToClear = this.axNodeDataCache_[update.nodeIdToClear];
544       if (!nodeToClear) {
545         logging.WARNING('Bad nodeIdToClear: ' + update.nodeIdToClear +
546                         ' (not in cache)');
547         lastError.set('automation',
548                       'Bad update received on automation tree',
549                       null,
550                       chrome);
551         return false;
552       }
553       if (nodeToClear === this.wrapper) {
554         this.invalidate_(nodeToClear);
555       } else {
556         var children = nodeToClear.children;
557         for (var i = 0; i < children.length; i++)
558           this.invalidate_(children[i]);
559         var nodeToClearImpl = privates(nodeToClear).impl;
560         nodeToClearImpl.childIds = []
561         updateState.pendingNodes[nodeToClearImpl.id] = nodeToClear;
562       }
563     }
565     for (var i = 0; i < update.nodes.length; i++) {
566       if (!this.updateNode_(update.nodes[i], updateState))
567         return false;
568     }
570     if (Object.keys(updateState.pendingNodes).length > 0) {
571       logging.WARNING('Nodes left pending by the update: ' +
572           $JSON.stringify(updateState.pendingNodes));
573       lastError.set('automation',
574                     'Bad update received on automation tree',
575                     null,
576                     chrome);
577       return false;
578     }
580     // Notify tree change observers of new nodes.
581     // TODO(dmazzoni): Notify tree change observers of changed nodes,
582     // and handle subtreeCreated and nodeCreated properly.
583     var observers = automationUtil.treeChangeObservers;
584     if (observers.length > 0) {
585       for (var nodeId in updateState.newNodes) {
586         var node = updateState.newNodes[nodeId];
587         var treeChange =
588             {target: node, type: schema.TreeChangeType.nodeCreated};
589         for (var i = 0; i < observers.length; i++) {
590           try {
591             observers[i](treeChange);
592           } catch (e) {
593             console.error('Error in tree change observer for ' +
594                 treeChange.type + ': ' + e.message +
595                 '\nStack trace: ' + e.stack);
596           }
597         }
598       }
599     }
601     return true;
602   },
604   destroy: function() {
605     if (this.hostTree)
606       this.hostTree.childTree = undefined;
607     this.hostTree = undefined;
609     this.dispatchEvent(schema.EventType.destroyed);
610     this.invalidate_(this.wrapper);
611   },
613   onAccessibilityEvent: function(eventParams) {
614     if (!this.unserialize(eventParams.update)) {
615       logging.WARNING('unserialization failed');
616       return false;
617     }
619     var targetNode = this.get(eventParams.targetID);
620     if (targetNode) {
621       var targetNodeImpl = privates(targetNode).impl;
622       targetNodeImpl.dispatchEvent(eventParams.eventType);
623     } else {
624       logging.WARNING('Got ' + eventParams.eventType +
625                       ' event on unknown node: ' + eventParams.targetID +
626                       '; this: ' + this.id);
627     }
628     return true;
629   },
631   toString: function() {
632     function toStringInternal(node, indent) {
633       if (!node)
634         return '';
635       var output =
636           new Array(indent).join(' ') +
637           AutomationNodeImpl.prototype.toString.call(node) +
638           '\n';
639       indent += 2;
640       for (var i = 0; i < node.children.length; i++)
641         output += toStringInternal(node.children[i], indent);
642       return output;
643     }
644     return toStringInternal(this, 0);
645   },
647   invalidate_: function(node) {
648     if (!node)
649       return;
651     // Notify tree change observers of the removed node.
652     var observers = automationUtil.treeChangeObservers;
653     if (observers.length > 0) {
654       var treeChange = {target: node, type: schema.TreeChangeType.nodeRemoved};
655       for (var i = 0; i < observers.length; i++) {
656         try {
657           observers[i](treeChange);
658         } catch (e) {
659           console.error('Error in tree change observer for ' + treeChange.type +
660               ': ' + e.message + '\nStack trace: ' + e.stack);
661         }
662       }
663     }
665     var children = node.children;
667     for (var i = 0, child; child = children[i]; i++) {
668       // Do not invalidate into subrooted nodes.
669       // TODO(dtseng): Revisit logic once out of proc iframes land.
670       if (child.root != node.root)
671         continue;
672       this.invalidate_(child);
673     }
675     // Retrieve the internal AutomationNodeImpl instance for this node.
676     // This object is not accessible outside of bindings code, but we can access
677     // it here.
678     var nodeImpl = privates(node).impl;
679     var id = nodeImpl.id;
680     for (var key in AutomationAttributeDefaults) {
681       nodeImpl[key] = AutomationAttributeDefaults[key];
682     }
684     nodeImpl.attributesInternal = {};
685     for (var key in DefaultMixinAttributes) {
686       var mixinAttribute = DefaultMixinAttributes[key];
687       if (!mixinAttribute.isRef)
688         nodeImpl.attributesInternal[key] = mixinAttribute.default;
689     }
690     nodeImpl.childIds = [];
691     nodeImpl.id = id;
692     delete this.axNodeDataCache_[id];
693   },
695   deleteOldChildren_: function(node, newChildIds) {
696     // Create a set of child ids in |src| for fast lookup, and return false
697     // if a duplicate is found;
698     var newChildIdSet = {};
699     for (var i = 0; i < newChildIds.length; i++) {
700       var childId = newChildIds[i];
701       if (newChildIdSet[childId]) {
702         logging.WARNING('Node ' + privates(node).impl.id +
703                         ' has duplicate child id ' + childId);
704         lastError.set('automation',
705                       'Bad update received on automation tree',
706                       null,
707                       chrome);
708         return false;
709       }
710       newChildIdSet[newChildIds[i]] = true;
711     }
713     // Delete the old children.
714     var nodeImpl = privates(node).impl;
715     var oldChildIds = nodeImpl.childIds;
716     for (var i = 0; i < oldChildIds.length;) {
717       var oldId = oldChildIds[i];
718       if (!newChildIdSet[oldId]) {
719         this.invalidate_(this.axNodeDataCache_[oldId]);
720         oldChildIds.splice(i, 1);
721       } else {
722         i++;
723       }
724     }
725     nodeImpl.childIds = oldChildIds;
727     return true;
728   },
730   createNewChildren_: function(node, newChildIds, updateState) {
731     logging.CHECK(node);
732     var success = true;
734     for (var i = 0; i < newChildIds.length; i++) {
735       var childId = newChildIds[i];
736       var childNode = this.axNodeDataCache_[childId];
737       if (childNode) {
738         if (childNode.parent != node) {
739           var parentId = -1;
740           if (childNode.parent) {
741             var parentImpl = privates(childNode.parent).impl;
742             parentId = parentImpl.id;
743           }
744           // This is a serious error - nodes should never be reparented.
745           // If this case occurs, continue so this node isn't left in an
746           // inconsistent state, but return failure at the end.
747           logging.WARNING('Node ' + childId + ' reparented from ' +
748                           parentId + ' to ' + privates(node).impl.id);
749           lastError.set('automation',
750                         'Bad update received on automation tree',
751                         null,
752                         chrome);
753           success = false;
754           continue;
755         }
756       } else {
757         childNode = new AutomationNode(this);
758         this.axNodeDataCache_[childId] = childNode;
759         privates(childNode).impl.id = childId;
760         updateState.pendingNodes[childId] = childNode;
761         updateState.newNodes[childId] = childNode;
762       }
763       privates(childNode).impl.indexInParent = i;
764       privates(childNode).impl.parentID = privates(node).impl.id;
765     }
767     return success;
768   },
770   setData_: function(node, nodeData) {
771     var nodeImpl = privates(node).impl;
773     // TODO(dtseng): Make into set listing all hosting node roles.
774     if (nodeData.role == schema.RoleType.webView) {
775       if (nodeImpl.childTreeID !== nodeData.intAttributes.childTreeId)
776         nodeImpl.pendingChildFrame = true;
778       if (nodeImpl.pendingChildFrame) {
779         nodeImpl.childTreeID = nodeData.intAttributes.childTreeId;
780         automationUtil.storeTreeCallback(nodeImpl.childTreeID, function(root) {
781           nodeImpl.pendingChildFrame = false;
782           nodeImpl.childTree = root;
783           privates(root).impl.hostTree = node;
784           if (root.attributes.docLoadingProgress == 1)
785             privates(root).impl.dispatchEvent(schema.EventType.loadComplete);
786           nodeImpl.dispatchEvent(schema.EventType.childrenChanged);
787         });
788         automationInternal.enableFrame(nodeImpl.childTreeID);
789       }
790     }
791     for (var key in AutomationAttributeDefaults) {
792       if (key in nodeData)
793         nodeImpl[key] = nodeData[key];
794       else
795         nodeImpl[key] = AutomationAttributeDefaults[key];
796     }
798     // Set all basic attributes.
799     this.mixinAttributes_(nodeImpl, DefaultMixinAttributes, nodeData);
801     // If this is a rootWebArea or webArea, set document attributes.
802     if (nodeData.role == schema.RoleType.rootWebArea ||
803         nodeData.role == schema.RoleType.webArea) {
804       this.mixinAttributes_(nodeImpl, DocumentMixinAttributes, nodeData);
805     }
807     // If this is a scrollable area, set scrollable attributes.
808     for (var scrollAttr in ScrollableMixinAttributes) {
809       var spec = ScrollableMixinAttributes[scrollAttr];
810       if (this.findAttribute_(scrollAttr, spec, nodeData) !== undefined) {
811         this.mixinAttributes_(nodeImpl, ScrollableMixinAttributes, nodeData);
812         break;
813       }
814     }
816     // If this is inside a live region, set live region mixins.
817     var attr = 'containerLiveStatus';
818     var spec = LiveRegionMixinAttributes[attr];
819     if (this.findAttribute_(attr, spec, nodeData) !== undefined) {
820       this.mixinAttributes_(nodeImpl, LiveRegionMixinAttributes, nodeData);
821     }
823     // If this is a link, set link attributes
824     if (nodeData.role == 'link') {
825       this.mixinAttributes_(nodeImpl, LinkMixinAttributes, nodeData);
826     }
828     // If this is an editable text area, set editable text attributes.
829     if (nodeData.role == schema.RoleType.textField) {
830       this.mixinAttributes_(nodeImpl, EditableTextMixinAttributes, nodeData);
831     }
833     // If this is a range type, set range attributes.
834     if (nodeData.role == schema.RoleType.progressIndicator ||
835         nodeData.role == schema.RoleType.scrollBar ||
836         nodeData.role == schema.RoleType.slider ||
837         nodeData.role == schema.RoleType.spinButton) {
838       this.mixinAttributes_(nodeImpl, RangeMixinAttributes, nodeData);
839     }
841     // If this is a table, set table attributes.
842     if (nodeData.role == schema.RoleType.table) {
843       this.mixinAttributes_(nodeImpl, TableMixinAttributes, nodeData);
844     }
846     // If this is a table cell, set table cell attributes.
847     if (nodeData.role == schema.RoleType.cell) {
848       this.mixinAttributes_(nodeImpl, TableCellMixinAttributes, nodeData);
849     }
851     // If this has an active descendant, expose it.
852     if ('intAttributes' in nodeData &&
853         'activedescendantId' in nodeData.intAttributes) {
854       this.mixinAttributes_(nodeImpl, ActiveDescendantMixinAttribute, nodeData);
855     }
857     for (var i = 0; i < AutomationAttributeTypes.length; i++) {
858       var attributeType = AutomationAttributeTypes[i];
859       for (var attributeName in nodeData[attributeType]) {
860         nodeImpl.attributesInternal[attributeName] =
861             nodeData[attributeType][attributeName];
862         if (ATTRIBUTE_BLACKLIST.hasOwnProperty(attributeName) ||
863             nodeImpl.attributes.hasOwnProperty(attributeName)) {
864           continue;
865         } else if (
866           ATTRIBUTE_NAME_TO_ID_ATTRIBUTE.hasOwnProperty(attributeName)) {
867           this.defineReadonlyAttribute_(nodeImpl,
868                                         nodeImpl.attributes,
869                                         attributeName,
870                                         true);
871         } else {
872           this.defineReadonlyAttribute_(nodeImpl,
873                                         nodeImpl.attributes,
874                                         attributeName);
875         }
876       }
877     }
878   },
880   mixinAttributes_: function(nodeImpl, attributes, nodeData) {
881     for (var attribute in attributes) {
882       var spec = attributes[attribute];
883       if (spec.isRef)
884         this.mixinRelationshipAttribute_(nodeImpl, attribute, spec, nodeData);
885       else
886         this.mixinAttribute_(nodeImpl, attribute, spec, nodeData);
887     }
888   },
890   mixinAttribute_: function(nodeImpl, attribute, spec, nodeData) {
891     var value = this.findAttribute_(attribute, spec, nodeData);
892     if (value === undefined)
893       value = spec.default;
894     nodeImpl.attributesInternal[attribute] = value;
895     this.defineReadonlyAttribute_(nodeImpl, nodeImpl, attribute);
896   },
898   mixinRelationshipAttribute_: function(nodeImpl, attribute, spec, nodeData) {
899     var idAttribute = spec.idAttribute;
900     var idValue = spec.default;
901     if (spec.idFrom in nodeData) {
902       idValue = idAttribute in nodeData[spec.idFrom]
903           ? nodeData[spec.idFrom][idAttribute] : idValue;
904     }
906     // Ok to define a list attribute with an empty list, but not a
907     // single ref with a null ID.
908     if (idValue === null)
909       return;
911     nodeImpl.attributesInternal[idAttribute] = idValue;
912     this.defineReadonlyAttribute_(
913       nodeImpl, nodeImpl, attribute, true, idAttribute);
914   },
916   findAttribute_: function(attribute, spec, nodeData) {
917     if (!('reflectFrom' in spec))
918       return;
919     var attributeGroup = spec.reflectFrom;
920     if (!(attributeGroup in nodeData))
921       return;
923     return nodeData[attributeGroup][attribute];
924   },
926   defineReadonlyAttribute_: function(
927       node, object, attributeName, opt_isIDRef, opt_idAttribute) {
928     if (attributeName in object)
929       return;
931     if (opt_isIDRef) {
932       $Object.defineProperty(object, attributeName, {
933         enumerable: true,
934         get: function() {
935           var idAttribute = opt_idAttribute ||
936                             ATTRIBUTE_NAME_TO_ID_ATTRIBUTE[attributeName];
937           var idValue = node.attributesInternal[idAttribute];
938           if (Array.isArray(idValue)) {
939             return idValue.map(function(current) {
940               return node.rootImpl.get(current);
941             }, this);
942           }
943           return node.rootImpl.get(idValue);
944         }.bind(this),
945       });
946     } else {
947       $Object.defineProperty(object, attributeName, {
948         enumerable: true,
949         get: function() {
950           return node.attributesInternal[attributeName];
951         }.bind(this),
952       });
953     }
955     if (object instanceof AutomationNodeImpl) {
956       // Also expose attribute publicly on the wrapper.
957       $Object.defineProperty(object.wrapper, attributeName, {
958         enumerable: true,
959         get: function() {
960           return object[attributeName];
961         },
962       });
964     }
965   },
967   updateNode_: function(nodeData, updateState) {
968     var node = this.axNodeDataCache_[nodeData.id];
969     var didUpdateRoot = false;
970     if (node) {
971       delete updateState.pendingNodes[privates(node).impl.id];
972     } else {
973       if (nodeData.role != schema.RoleType.rootWebArea &&
974           nodeData.role != schema.RoleType.desktop) {
975         logging.WARNING(String(nodeData.id) +
976                      ' is not in the cache and not the new root.');
977         lastError.set('automation',
978                       'Bad update received on automation tree',
979                       null,
980                       chrome);
981         return false;
982       }
983       // |this| is an AutomationRootNodeImpl; retrieve the
984       // AutomationRootNode instance instead.
985       node = this.wrapper;
986       didUpdateRoot = true;
987       updateState.newNodes[this.id] = this.wrapper;
988     }
989     this.setData_(node, nodeData);
991     // TODO(aboxhall): send onChanged event?
992     logging.CHECK(node);
993     if (!this.deleteOldChildren_(node, nodeData.childIds)) {
994       if (didUpdateRoot) {
995         this.invalidate_(this.wrapper);
996       }
997       return false;
998     }
999     var nodeImpl = privates(node).impl;
1001     var success = this.createNewChildren_(node,
1002                                           nodeData.childIds,
1003                                           updateState);
1004     nodeImpl.childIds = nodeData.childIds;
1005     this.axNodeDataCache_[nodeImpl.id] = node;
1007     return success;
1008   }
1012 var AutomationNode = utils.expose('AutomationNode',
1013                                   AutomationNodeImpl,
1014                                   { functions: ['doDefault',
1015                                                 'find',
1016                                                 'findAll',
1017                                                 'focus',
1018                                                 'makeVisible',
1019                                                 'matches',
1020                                                 'setSelection',
1021                                                 'addEventListener',
1022                                                 'removeEventListener',
1023                                                 'addTreeChangeObserver',
1024                                                 'removeTreeChangeObserver',
1025                                                 'domQuerySelector',
1026                                                 'toString' ],
1027                                     readonly: ['parent',
1028                                                'firstChild',
1029                                                'lastChild',
1030                                                'children',
1031                                                'previousSibling',
1032                                                'nextSibling',
1033                                                'isRootNode',
1034                                                'role',
1035                                                'state',
1036                                                'location',
1037                                                'attributes',
1038                                                'indexInParent',
1039                                                'root'] });
1041 var AutomationRootNode = utils.expose('AutomationRootNode',
1042                                       AutomationRootNodeImpl,
1043                                       { superclass: AutomationNode });
1045 exports.AutomationNode = AutomationNode;
1046 exports.AutomationRootNode = AutomationRootNode;