Ensure WebView notifies desktop automation on creation, destruction, and change
[chromium-blink-merge.git] / chrome / renderer / resources / extensions / automation / automation_node.js
blob309edacb36576db26dd0d674fd80e95144dbce5e
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)
489  * AutomationRootNode.
491  * An AutomationRootNode is the javascript end of an AXTree living in the
492  * browser. AutomationRootNode handles unserializing incremental updates from
493  * the source AXTree. Each update contains node data that form a complete tree
494  * after applying the update.
496  * A brief note about ids used through this class. The source AXTree assigns
497  * unique ids per node and we use these ids to build a hash to the actual
498  * AutomationNode object.
499  * Thus, tree traversals amount to a lookup in our hash.
501  * The tree itself is identified by the accessibility tree id of the
502  * renderer widget host.
503  * @constructor
504  */
505 function AutomationRootNodeImpl(treeID) {
506   AutomationNodeImpl.call(this, this);
507   this.treeID = treeID;
508   this.axNodeDataCache_ = {};
511 AutomationRootNodeImpl.prototype = {
512   __proto__: AutomationNodeImpl.prototype,
514   isRootNode: true,
515   treeID: -1,
517   get: function(id) {
518     if (id == undefined)
519       return undefined;
521     return this.axNodeDataCache_[id];
522   },
524   unserialize: function(update) {
525     var updateState = { pendingNodes: {}, newNodes: {} };
526     var oldRootId = this.id;
528     if (update.nodeIdToClear < 0) {
529         logging.WARNING('Bad nodeIdToClear: ' + update.nodeIdToClear);
530         lastError.set('automation',
531                       'Bad update received on automation tree',
532                       null,
533                       chrome);
534         return false;
535     } else if (update.nodeIdToClear > 0) {
536       var nodeToClear = this.axNodeDataCache_[update.nodeIdToClear];
537       if (!nodeToClear) {
538         logging.WARNING('Bad nodeIdToClear: ' + update.nodeIdToClear +
539                         ' (not in cache)');
540         lastError.set('automation',
541                       'Bad update received on automation tree',
542                       null,
543                       chrome);
544         return false;
545       }
546       if (nodeToClear === this.wrapper) {
547         this.invalidate_(nodeToClear);
548       } else {
549         var children = nodeToClear.children;
550         for (var i = 0; i < children.length; i++)
551           this.invalidate_(children[i]);
552         var nodeToClearImpl = privates(nodeToClear).impl;
553         nodeToClearImpl.childIds = []
554         updateState.pendingNodes[nodeToClearImpl.id] = nodeToClear;
555       }
556     }
558     for (var i = 0; i < update.nodes.length; i++) {
559       if (!this.updateNode_(update.nodes[i], updateState))
560         return false;
561     }
563     if (Object.keys(updateState.pendingNodes).length > 0) {
564       logging.WARNING('Nodes left pending by the update: ' +
565           $JSON.stringify(updateState.pendingNodes));
566       lastError.set('automation',
567                     'Bad update received on automation tree',
568                     null,
569                     chrome);
570       return false;
571     }
572     return true;
573   },
575   destroy: function() {
576     if (this.hostTree)
577       this.hostTree.childTree = undefined;
578     this.hostTree = undefined;
580     this.dispatchEvent(schema.EventType.destroyed);
581     this.invalidate_(this.wrapper);
582   },
584   onAccessibilityEvent: function(eventParams) {
585     if (!this.unserialize(eventParams.update)) {
586       logging.WARNING('unserialization failed');
587       return false;
588     }
590     var targetNode = this.get(eventParams.targetID);
591     if (targetNode) {
592       var targetNodeImpl = privates(targetNode).impl;
593       targetNodeImpl.dispatchEvent(eventParams.eventType);
594     } else {
595       logging.WARNING('Got ' + eventParams.eventType +
596                       ' event on unknown node: ' + eventParams.targetID +
597                       '; this: ' + this.id);
598     }
599     return true;
600   },
602   toString: function() {
603     function toStringInternal(node, indent) {
604       if (!node)
605         return '';
606       var output =
607           new Array(indent).join(' ') +
608           AutomationNodeImpl.prototype.toString.call(node) +
609           '\n';
610       indent += 2;
611       for (var i = 0; i < node.children.length; i++)
612         output += toStringInternal(node.children[i], indent);
613       return output;
614     }
615     return toStringInternal(this, 0);
616   },
618   invalidate_: function(node) {
619     if (!node)
620       return;
621     var children = node.children;
623     for (var i = 0, child; child = children[i]; i++) {
624       // Do not invalidate into subrooted nodes.
625       // TODO(dtseng): Revisit logic once out of proc iframes land.
626       if (child.root != node.root)
627         continue;
628       this.invalidate_(child);
629     }
631     // Retrieve the internal AutomationNodeImpl instance for this node.
632     // This object is not accessible outside of bindings code, but we can access
633     // it here.
634     var nodeImpl = privates(node).impl;
635     var id = nodeImpl.id;
636     for (var key in AutomationAttributeDefaults) {
637       nodeImpl[key] = AutomationAttributeDefaults[key];
638     }
640     nodeImpl.attributesInternal = {};
641     for (var key in DefaultMixinAttributes) {
642       var mixinAttribute = DefaultMixinAttributes[key];
643       if (!mixinAttribute.isRef)
644         nodeImpl.attributesInternal[key] = mixinAttribute.default;
645     }
646     nodeImpl.childIds = [];
647     nodeImpl.id = id;
648     delete this.axNodeDataCache_[id];
649   },
651   deleteOldChildren_: function(node, newChildIds) {
652     // Create a set of child ids in |src| for fast lookup, and return false
653     // if a duplicate is found;
654     var newChildIdSet = {};
655     for (var i = 0; i < newChildIds.length; i++) {
656       var childId = newChildIds[i];
657       if (newChildIdSet[childId]) {
658         logging.WARNING('Node ' + privates(node).impl.id +
659                         ' has duplicate child id ' + childId);
660         lastError.set('automation',
661                       'Bad update received on automation tree',
662                       null,
663                       chrome);
664         return false;
665       }
666       newChildIdSet[newChildIds[i]] = true;
667     }
669     // Delete the old children.
670     var nodeImpl = privates(node).impl;
671     var oldChildIds = nodeImpl.childIds;
672     for (var i = 0; i < oldChildIds.length;) {
673       var oldId = oldChildIds[i];
674       if (!newChildIdSet[oldId]) {
675         this.invalidate_(this.axNodeDataCache_[oldId]);
676         oldChildIds.splice(i, 1);
677       } else {
678         i++;
679       }
680     }
681     nodeImpl.childIds = oldChildIds;
683     return true;
684   },
686   createNewChildren_: function(node, newChildIds, updateState) {
687     logging.CHECK(node);
688     var success = true;
690     for (var i = 0; i < newChildIds.length; i++) {
691       var childId = newChildIds[i];
692       var childNode = this.axNodeDataCache_[childId];
693       if (childNode) {
694         if (childNode.parent != node) {
695           var parentId = -1;
696           if (childNode.parent) {
697             var parentImpl = privates(childNode.parent).impl;
698             parentId = parentImpl.id;
699           }
700           // This is a serious error - nodes should never be reparented.
701           // If this case occurs, continue so this node isn't left in an
702           // inconsistent state, but return failure at the end.
703           logging.WARNING('Node ' + childId + ' reparented from ' +
704                           parentId + ' to ' + privates(node).impl.id);
705           lastError.set('automation',
706                         'Bad update received on automation tree',
707                         null,
708                         chrome);
709           success = false;
710           continue;
711         }
712       } else {
713         childNode = new AutomationNode(this);
714         this.axNodeDataCache_[childId] = childNode;
715         privates(childNode).impl.id = childId;
716         updateState.pendingNodes[childId] = childNode;
717         updateState.newNodes[childId] = childNode;
718       }
719       privates(childNode).impl.indexInParent = i;
720       privates(childNode).impl.parentID = privates(node).impl.id;
721     }
723     return success;
724   },
726   setData_: function(node, nodeData) {
727     var nodeImpl = privates(node).impl;
729     // TODO(dtseng): Make into set listing all hosting node roles.
730     if (nodeData.role == schema.RoleType.webView) {
731       if (nodeImpl.childTreeID !== nodeData.intAttributes.childTreeId)
732         nodeImpl.pendingChildFrame = true;
734       if (nodeImpl.pendingChildFrame) {
735         nodeImpl.childTreeID = nodeData.intAttributes.childTreeId;
736         automationInternal.enableFrame(nodeImpl.childTreeID);
737         automationUtil.storeTreeCallback(nodeImpl.childTreeID, function(root) {
738           nodeImpl.pendingChildFrame = false;
739           nodeImpl.childTree = root;
740           privates(root).impl.hostTree = node;
741           nodeImpl.dispatchEvent(schema.EventType.childrenChanged);
742         });
743       }
744     }
745     for (var key in AutomationAttributeDefaults) {
746       if (key in nodeData)
747         nodeImpl[key] = nodeData[key];
748       else
749         nodeImpl[key] = AutomationAttributeDefaults[key];
750     }
752     // Set all basic attributes.
753     this.mixinAttributes_(nodeImpl, DefaultMixinAttributes, nodeData);
755     // If this is a rootWebArea or webArea, set document attributes.
756     if (nodeData.role == schema.RoleType.rootWebArea ||
757         nodeData.role == schema.RoleType.webArea) {
758       this.mixinAttributes_(nodeImpl, DocumentMixinAttributes, nodeData);
759     }
761     // If this is a scrollable area, set scrollable attributes.
762     for (var scrollAttr in ScrollableMixinAttributes) {
763       var spec = ScrollableMixinAttributes[scrollAttr];
764       if (this.findAttribute_(scrollAttr, spec, nodeData) !== undefined) {
765         this.mixinAttributes_(nodeImpl, ScrollableMixinAttributes, nodeData);
766         break;
767       }
768     }
770     // If this is a link, set link attributes
771     if (nodeData.role == 'link') {
772       this.mixinAttributes_(nodeImpl, LinkMixinAttributes, nodeData);
773     }
775     // If this is an editable text area, set editable text attributes.
776     if (nodeData.role == schema.RoleType.textField ||
777         nodeData.role == schema.RoleType.textArea) {
778       this.mixinAttributes_(nodeImpl, EditableTextMixinAttributes, nodeData);
779     }
781     // If this is a range type, set range attributes.
782     if (nodeData.role == schema.RoleType.progressIndicator ||
783         nodeData.role == schema.RoleType.scrollBar ||
784         nodeData.role == schema.RoleType.slider ||
785         nodeData.role == schema.RoleType.spinButton) {
786       this.mixinAttributes_(nodeImpl, RangeMixinAttributes, nodeData);
787     }
789     // If this is a table, set table attributes.
790     if (nodeData.role == schema.RoleType.table) {
791       this.mixinAttributes_(nodeImpl, TableMixinAttributes, nodeData);
792     }
794     // If this is a table cell, set table cell attributes.
795     if (nodeData.role == schema.RoleType.cell) {
796       this.mixinAttributes_(nodeImpl, TableCellMixinAttributes, nodeData);
797     }
799     // If this has an active descendant, expose it.
800     if ('intAttributes' in nodeData &&
801         'activedescendantId' in nodeData.intAttributes) {
802       this.mixinAttributes_(nodeImpl, ActiveDescendantMixinAttribute, nodeData);
803     }
805     for (var i = 0; i < AutomationAttributeTypes.length; i++) {
806       var attributeType = AutomationAttributeTypes[i];
807       for (var attributeName in nodeData[attributeType]) {
808         nodeImpl.attributesInternal[attributeName] =
809             nodeData[attributeType][attributeName];
810         if (ATTRIBUTE_BLACKLIST.hasOwnProperty(attributeName) ||
811             nodeImpl.attributes.hasOwnProperty(attributeName)) {
812           continue;
813         } else if (
814           ATTRIBUTE_NAME_TO_ID_ATTRIBUTE.hasOwnProperty(attributeName)) {
815           this.defineReadonlyAttribute_(nodeImpl,
816                                         nodeImpl.attributes,
817                                         attributeName,
818                                         true);
819         } else {
820           this.defineReadonlyAttribute_(nodeImpl,
821                                         nodeImpl.attributes,
822                                         attributeName);
823         }
824       }
825     }
826   },
828   mixinAttributes_: function(nodeImpl, attributes, nodeData) {
829     for (var attribute in attributes) {
830       var spec = attributes[attribute];
831       if (spec.isRef)
832         this.mixinRelationshipAttribute_(nodeImpl, attribute, spec, nodeData);
833       else
834         this.mixinAttribute_(nodeImpl, attribute, spec, nodeData);
835     }
836   },
838   mixinAttribute_: function(nodeImpl, attribute, spec, nodeData) {
839     var value = this.findAttribute_(attribute, spec, nodeData);
840     if (value === undefined)
841       value = spec.default;
842     nodeImpl.attributesInternal[attribute] = value;
843     this.defineReadonlyAttribute_(nodeImpl, nodeImpl, attribute);
844   },
846   mixinRelationshipAttribute_: function(nodeImpl, attribute, spec, nodeData) {
847     var idAttribute = spec.idAttribute;
848     var idValue = spec.default;
849     if (spec.idFrom in nodeData) {
850       idValue = idAttribute in nodeData[spec.idFrom]
851           ? nodeData[spec.idFrom][idAttribute] : idValue;
852     }
854     // Ok to define a list attribute with an empty list, but not a
855     // single ref with a null ID.
856     if (idValue === null)
857       return;
859     nodeImpl.attributesInternal[idAttribute] = idValue;
860     this.defineReadonlyAttribute_(
861       nodeImpl, nodeImpl, attribute, true, idAttribute);
862   },
864   findAttribute_: function(attribute, spec, nodeData) {
865     if (!('reflectFrom' in spec))
866       return;
867     var attributeGroup = spec.reflectFrom;
868     if (!(attributeGroup in nodeData))
869       return;
871     return nodeData[attributeGroup][attribute];
872   },
874   defineReadonlyAttribute_: function(
875       node, object, attributeName, opt_isIDRef, opt_idAttribute) {
876     if (attributeName in object)
877       return;
879     if (opt_isIDRef) {
880       $Object.defineProperty(object, attributeName, {
881         enumerable: true,
882         get: function() {
883           var idAttribute = opt_idAttribute ||
884                             ATTRIBUTE_NAME_TO_ID_ATTRIBUTE[attributeName];
885           var idValue = node.attributesInternal[idAttribute];
886           if (Array.isArray(idValue)) {
887             return idValue.map(function(current) {
888               return node.rootImpl.get(current);
889             }, this);
890           }
891           return node.rootImpl.get(idValue);
892         }.bind(this),
893       });
894     } else {
895       $Object.defineProperty(object, attributeName, {
896         enumerable: true,
897         get: function() {
898           return node.attributesInternal[attributeName];
899         }.bind(this),
900       });
901     }
903     if (object instanceof AutomationNodeImpl) {
904       // Also expose attribute publicly on the wrapper.
905       $Object.defineProperty(object.wrapper, attributeName, {
906         enumerable: true,
907         get: function() {
908           return object[attributeName];
909         },
910       });
912     }
913   },
915   updateNode_: function(nodeData, updateState) {
916     var node = this.axNodeDataCache_[nodeData.id];
917     var didUpdateRoot = false;
918     if (node) {
919       delete updateState.pendingNodes[privates(node).impl.id];
920     } else {
921       if (nodeData.role != schema.RoleType.rootWebArea &&
922           nodeData.role != schema.RoleType.desktop) {
923         logging.WARNING(String(nodeData.id) +
924                      ' is not in the cache and not the new root.');
925         lastError.set('automation',
926                       'Bad update received on automation tree',
927                       null,
928                       chrome);
929         return false;
930       }
931       // |this| is an AutomationRootNodeImpl; retrieve the
932       // AutomationRootNode instance instead.
933       node = this.wrapper;
934       didUpdateRoot = true;
935       updateState.newNodes[this.id] = this.wrapper;
936     }
937     this.setData_(node, nodeData);
939     // TODO(aboxhall): send onChanged event?
940     logging.CHECK(node);
941     if (!this.deleteOldChildren_(node, nodeData.childIds)) {
942       if (didUpdateRoot) {
943         this.invalidate_(this.wrapper);
944       }
945       return false;
946     }
947     var nodeImpl = privates(node).impl;
949     var success = this.createNewChildren_(node,
950                                           nodeData.childIds,
951                                           updateState);
952     nodeImpl.childIds = nodeData.childIds;
953     this.axNodeDataCache_[nodeImpl.id] = node;
955     return success;
956   }
960 var AutomationNode = utils.expose('AutomationNode',
961                                   AutomationNodeImpl,
962                                   { functions: ['doDefault',
963                                                 'find',
964                                                 'findAll',
965                                                 'focus',
966                                                 'makeVisible',
967                                                 'matches',
968                                                 'setSelection',
969                                                 'addEventListener',
970                                                 'removeEventListener',
971                                                 'domQuerySelector',
972                                                 'toJSON' ],
973                                     readonly: ['parent',
974                                                'firstChild',
975                                                'lastChild',
976                                                'children',
977                                                'previousSibling',
978                                                'nextSibling',
979                                                'isRootNode',
980                                                'role',
981                                                'state',
982                                                'location',
983                                                'attributes',
984                                                'indexInParent',
985                                                'root'] });
987 var AutomationRootNode = utils.expose('AutomationRootNode',
988                                       AutomationRootNodeImpl,
989                                       { superclass: AutomationNode });
991 exports.AutomationNode = AutomationNode;
992 exports.AutomationRootNode = AutomationRootNode;