1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 var AutomationEvent = require('automationEvent').AutomationEvent;
6 var automationInternal =
7 require('binding').Binding.create('automationInternal').generate();
8 var IsInteractPermitted =
9 requireNative('automationInternal').IsInteractPermitted;
11 var lastError = require('lastError');
12 var logging = requireNative('logging');
13 var schema = requireNative('automationInternal').GetSchemaAdditions();
14 var utils = require('utils');
17 * A single node in the Automation tree.
18 * @param {AutomationRootNodeImpl} root The root of the tree.
21 function AutomationNodeImpl(root) {
24 // Public attributes. No actual data gets set on this object.
26 // Internal object holding all attributes.
27 this.attributesInternal = {};
29 this.location = { left: 0, top: 0, width: 0, height: 0 };
32 AutomationNodeImpl.prototype = {
35 state: { busy: true },
39 return this.rootImpl.wrapper;
43 return this.hostTree || this.rootImpl.get(this.parentID);
47 return this.childTree || this.rootImpl.get(this.childIds[0]);
51 var childIds = this.childIds;
52 return this.childTree || this.rootImpl.get(childIds[childIds.length - 1]);
57 return [this.childTree];
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));
67 get previousSibling() {
68 var parent = this.parent;
69 if (parent && this.indexInParent > 0)
70 return parent.children[this.indexInParent - 1];
75 var parent = this.parent;
76 if (parent && this.indexInParent < parent.children.length)
77 return parent.children[this.indexInParent + 1];
81 doDefault: function() {
82 this.performAction_('doDefault');
86 this.performAction_('focus');
89 makeVisible: function() {
90 this.performAction_('makeVisible');
93 setSelection: function(startIndex, endIndex) {
94 this.performAction_('setSelection',
95 { startIndex: startIndex,
96 endIndex: endIndex });
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));
107 find: function(params) {
108 return this.findInternal_(params);
111 findAll: function(params) {
112 return this.findInternal_(params, []);
115 matches: function(params) {
116 return this.matchInternal_(params);
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});
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);
138 return { treeID: this.treeID,
141 attributes: this.attributes };
144 dispatchEvent: function(eventType) {
146 var parent = this.parent;
149 parent = parent.parent;
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);
165 toString: function() {
166 var impl = privates(this).impl;
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);
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)
187 dispatchEventAtTargeting_: function(event) {
188 privates(event).impl.eventPhase = Event.AT_TARGET;
189 this.fireEventListeners_(this.wrapper, event);
190 return !privates(event).impl.propagationStopped;
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)
203 fireEventListeners_: function(node, event) {
204 var nodeImpl = privates(node).impl;
205 var listeners = nodeImpl.listeners[event.type];
208 var eventPhase = event.eventPhase;
209 for (var i = 0; i < listeners.length; i++) {
210 if (eventPhase == Event.CAPTURING_PHASE && !listeners[i].capture)
212 if (eventPhase == Event.BUBBLING_PHASE && listeners[i].capture)
216 listeners[i].callback(event);
218 console.error('Error in event handler for ' + event.type +
219 'during phase ' + eventPhase + ': ' +
220 e.message + '\nStack trace: ' + e.stack);
225 performAction_: function(actionType, opt_args) {
226 // Not yet initialized.
227 if (this.rootImpl.treeID === undefined ||
228 this.id === undefined) {
232 // Check permissions.
233 if (!IsInteractPermitted()) {
234 throw new Error(actionType + ' requires {"desktop": true} or' +
235 ' {"interact": true} in the "automation" manifest key.');
238 automationInternal.performAction({ treeID: this.rootImpl.treeID,
239 automationNodeID: this.id,
240 actionType: actionType },
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
248 if (!resultAutomationNodeID) {
252 var resultNode = this.rootImpl.get(resultAutomationNodeID);
254 logging.WARNING('Query selector result not in tree: ' +
255 resultAutomationNodeID);
258 userCallback(resultNode);
261 findInternal_: function(params, opt_results) {
263 this.forAllDescendants_(function(node) {
264 if (privates(node).impl.matchInternal_(params)) {
266 opt_results.push(node);
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.
283 forAllDescendants_: function(closure) {
284 var stack = this.wrapper.children.reverse();
285 while (stack.length > 0) {
286 var node = stack.pop();
290 var children = node.children;
291 for (var i = children.length - 1; i >= 0; i--)
292 stack.push(children[i]);
296 matchInternal_: function(params) {
297 if (Object.keys(params).length == 0)
300 if ('role' in params && this.role != params.role)
303 if ('state' in params) {
304 for (var state in params.state) {
305 if (params.state[state] != (state in this.state))
309 if ('attributes' in params) {
310 for (var attribute in params.attributes) {
311 if (!(attribute in this.attributesInternal))
314 var attrValue = params.attributes[attribute];
315 if (typeof attrValue != 'object') {
316 if (this.attributesInternal[attribute] !== attrValue)
318 } else if (attrValue instanceof RegExp) {
319 if (typeof this.attributesInternal[attribute] != 'string')
321 if (!attrValue.test(this.attributesInternal[attribute]))
324 // TODO(aboxhall): handle intlist case.
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 = {
339 'location': { left: 0, top: 0, width: 0, height: 0 }
343 var AutomationAttributeTypes = [
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>}
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>}
372 var ATTRIBUTE_BLACKLIST = {'activedescendantId': true,
375 'describedbyIds': true,
377 'labelledbyIds': 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,
419 function defaultNodeRefListAttribute(idAttribute, opt_defaultVal) {
420 var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : [];
421 return { default: [],
422 idFrom: 'intlistAttributes',
423 idAttribute: idAttribute,
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.
505 function AutomationRootNodeImpl(treeID) {
506 AutomationNodeImpl.call(this, this);
507 this.treeID = treeID;
508 this.axNodeDataCache_ = {};
511 AutomationRootNodeImpl.prototype = {
512 __proto__: AutomationNodeImpl.prototype,
521 return this.axNodeDataCache_[id];
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',
535 } else if (update.nodeIdToClear > 0) {
536 var nodeToClear = this.axNodeDataCache_[update.nodeIdToClear];
538 logging.WARNING('Bad nodeIdToClear: ' + update.nodeIdToClear +
540 lastError.set('automation',
541 'Bad update received on automation tree',
546 if (nodeToClear === this.wrapper) {
547 this.invalidate_(nodeToClear);
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;
558 for (var i = 0; i < update.nodes.length; i++) {
559 if (!this.updateNode_(update.nodes[i], updateState))
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',
575 destroy: function() {
577 this.hostTree.childTree = undefined;
578 this.hostTree = undefined;
580 this.dispatchEvent(schema.EventType.destroyed);
581 this.invalidate_(this.wrapper);
584 onAccessibilityEvent: function(eventParams) {
585 if (!this.unserialize(eventParams.update)) {
586 logging.WARNING('unserialization failed');
590 var targetNode = this.get(eventParams.targetID);
592 var targetNodeImpl = privates(targetNode).impl;
593 targetNodeImpl.dispatchEvent(eventParams.eventType);
595 logging.WARNING('Got ' + eventParams.eventType +
596 ' event on unknown node: ' + eventParams.targetID +
597 '; this: ' + this.id);
602 toString: function() {
603 function toStringInternal(node, indent) {
607 new Array(indent).join(' ') +
608 AutomationNodeImpl.prototype.toString.call(node) +
611 for (var i = 0; i < node.children.length; i++)
612 output += toStringInternal(node.children[i], indent);
615 return toStringInternal(this, 0);
618 invalidate_: function(node) {
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)
628 this.invalidate_(child);
631 // Retrieve the internal AutomationNodeImpl instance for this node.
632 // This object is not accessible outside of bindings code, but we can access
634 var nodeImpl = privates(node).impl;
635 var id = nodeImpl.id;
636 for (var key in AutomationAttributeDefaults) {
637 nodeImpl[key] = AutomationAttributeDefaults[key];
640 nodeImpl.attributesInternal = {};
641 for (var key in DefaultMixinAttributes) {
642 var mixinAttribute = DefaultMixinAttributes[key];
643 if (!mixinAttribute.isRef)
644 nodeImpl.attributesInternal[key] = mixinAttribute.default;
646 nodeImpl.childIds = [];
648 delete this.axNodeDataCache_[id];
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',
666 newChildIdSet[newChildIds[i]] = true;
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);
681 nodeImpl.childIds = oldChildIds;
686 createNewChildren_: function(node, newChildIds, updateState) {
690 for (var i = 0; i < newChildIds.length; i++) {
691 var childId = newChildIds[i];
692 var childNode = this.axNodeDataCache_[childId];
694 if (childNode.parent != node) {
696 if (childNode.parent) {
697 var parentImpl = privates(childNode.parent).impl;
698 parentId = parentImpl.id;
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',
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;
719 privates(childNode).impl.indexInParent = i;
720 privates(childNode).impl.parentID = privates(node).impl.id;
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);
745 for (var key in AutomationAttributeDefaults) {
747 nodeImpl[key] = nodeData[key];
749 nodeImpl[key] = AutomationAttributeDefaults[key];
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);
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);
770 // If this is a link, set link attributes
771 if (nodeData.role == 'link') {
772 this.mixinAttributes_(nodeImpl, LinkMixinAttributes, nodeData);
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);
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);
789 // If this is a table, set table attributes.
790 if (nodeData.role == schema.RoleType.table) {
791 this.mixinAttributes_(nodeImpl, TableMixinAttributes, nodeData);
794 // If this is a table cell, set table cell attributes.
795 if (nodeData.role == schema.RoleType.cell) {
796 this.mixinAttributes_(nodeImpl, TableCellMixinAttributes, nodeData);
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);
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)) {
814 ATTRIBUTE_NAME_TO_ID_ATTRIBUTE.hasOwnProperty(attributeName)) {
815 this.defineReadonlyAttribute_(nodeImpl,
820 this.defineReadonlyAttribute_(nodeImpl,
828 mixinAttributes_: function(nodeImpl, attributes, nodeData) {
829 for (var attribute in attributes) {
830 var spec = attributes[attribute];
832 this.mixinRelationshipAttribute_(nodeImpl, attribute, spec, nodeData);
834 this.mixinAttribute_(nodeImpl, attribute, spec, nodeData);
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);
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;
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)
859 nodeImpl.attributesInternal[idAttribute] = idValue;
860 this.defineReadonlyAttribute_(
861 nodeImpl, nodeImpl, attribute, true, idAttribute);
864 findAttribute_: function(attribute, spec, nodeData) {
865 if (!('reflectFrom' in spec))
867 var attributeGroup = spec.reflectFrom;
868 if (!(attributeGroup in nodeData))
871 return nodeData[attributeGroup][attribute];
874 defineReadonlyAttribute_: function(
875 node, object, attributeName, opt_isIDRef, opt_idAttribute) {
876 if (attributeName in object)
880 $Object.defineProperty(object, attributeName, {
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);
891 return node.rootImpl.get(idValue);
895 $Object.defineProperty(object, attributeName, {
898 return node.attributesInternal[attributeName];
903 if (object instanceof AutomationNodeImpl) {
904 // Also expose attribute publicly on the wrapper.
905 $Object.defineProperty(object.wrapper, attributeName, {
908 return object[attributeName];
915 updateNode_: function(nodeData, updateState) {
916 var node = this.axNodeDataCache_[nodeData.id];
917 var didUpdateRoot = false;
919 delete updateState.pendingNodes[privates(node).impl.id];
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',
931 // |this| is an AutomationRootNodeImpl; retrieve the
932 // AutomationRootNode instance instead.
934 didUpdateRoot = true;
935 updateState.newNodes[this.id] = this.wrapper;
937 this.setData_(node, nodeData);
939 // TODO(aboxhall): send onChanged event?
941 if (!this.deleteOldChildren_(node, nodeData.childIds)) {
943 this.invalidate_(this.wrapper);
947 var nodeImpl = privates(node).impl;
949 var success = this.createNewChildren_(node,
952 nodeImpl.childIds = nodeData.childIds;
953 this.axNodeDataCache_[nodeImpl.id] = node;
960 var AutomationNode = utils.expose('AutomationNode',
962 { functions: ['doDefault',
970 'removeEventListener',
987 var AutomationRootNode = utils.expose('AutomationRootNode',
988 AutomationRootNodeImpl,
989 { superclass: AutomationNode });
991 exports.AutomationNode = AutomationNode;
992 exports.AutomationRootNode = AutomationRootNode;