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)
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.
512 function AutomationRootNodeImpl(treeID) {
513 AutomationNodeImpl.call(this, this);
514 this.treeID = treeID;
515 this.axNodeDataCache_ = {};
518 AutomationRootNodeImpl.prototype = {
519 __proto__: AutomationNodeImpl.prototype,
528 return this.axNodeDataCache_[id];
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',
542 } else if (update.nodeIdToClear > 0) {
543 var nodeToClear = this.axNodeDataCache_[update.nodeIdToClear];
545 logging.WARNING('Bad nodeIdToClear: ' + update.nodeIdToClear +
547 lastError.set('automation',
548 'Bad update received on automation tree',
553 if (nodeToClear === this.wrapper) {
554 this.invalidate_(nodeToClear);
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;
565 for (var i = 0; i < update.nodes.length; i++) {
566 if (!this.updateNode_(update.nodes[i], updateState))
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',
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];
588 {target: node, type: schema.TreeChangeType.nodeCreated};
589 for (var i = 0; i < observers.length; i++) {
591 observers[i](treeChange);
593 console.error('Error in tree change observer for ' +
594 treeChange.type + ': ' + e.message +
595 '\nStack trace: ' + e.stack);
604 destroy: function() {
606 this.hostTree.childTree = undefined;
607 this.hostTree = undefined;
609 this.dispatchEvent(schema.EventType.destroyed);
610 this.invalidate_(this.wrapper);
613 onAccessibilityEvent: function(eventParams) {
614 if (!this.unserialize(eventParams.update)) {
615 logging.WARNING('unserialization failed');
619 var targetNode = this.get(eventParams.targetID);
621 var targetNodeImpl = privates(targetNode).impl;
622 targetNodeImpl.dispatchEvent(eventParams.eventType);
624 logging.WARNING('Got ' + eventParams.eventType +
625 ' event on unknown node: ' + eventParams.targetID +
626 '; this: ' + this.id);
631 toString: function() {
632 function toStringInternal(node, indent) {
636 new Array(indent).join(' ') +
637 AutomationNodeImpl.prototype.toString.call(node) +
640 for (var i = 0; i < node.children.length; i++)
641 output += toStringInternal(node.children[i], indent);
644 return toStringInternal(this, 0);
647 invalidate_: function(node) {
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++) {
657 observers[i](treeChange);
659 console.error('Error in tree change observer for ' + treeChange.type +
660 ': ' + e.message + '\nStack trace: ' + e.stack);
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)
672 this.invalidate_(child);
675 // Retrieve the internal AutomationNodeImpl instance for this node.
676 // This object is not accessible outside of bindings code, but we can access
678 var nodeImpl = privates(node).impl;
679 var id = nodeImpl.id;
680 for (var key in AutomationAttributeDefaults) {
681 nodeImpl[key] = AutomationAttributeDefaults[key];
684 nodeImpl.attributesInternal = {};
685 for (var key in DefaultMixinAttributes) {
686 var mixinAttribute = DefaultMixinAttributes[key];
687 if (!mixinAttribute.isRef)
688 nodeImpl.attributesInternal[key] = mixinAttribute.default;
690 nodeImpl.childIds = [];
692 delete this.axNodeDataCache_[id];
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',
710 newChildIdSet[newChildIds[i]] = true;
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);
725 nodeImpl.childIds = oldChildIds;
730 createNewChildren_: function(node, newChildIds, updateState) {
734 for (var i = 0; i < newChildIds.length; i++) {
735 var childId = newChildIds[i];
736 var childNode = this.axNodeDataCache_[childId];
738 if (childNode.parent != node) {
740 if (childNode.parent) {
741 var parentImpl = privates(childNode.parent).impl;
742 parentId = parentImpl.id;
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',
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;
763 privates(childNode).impl.indexInParent = i;
764 privates(childNode).impl.parentID = privates(node).impl.id;
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);
788 automationInternal.enableFrame(nodeImpl.childTreeID);
791 for (var key in AutomationAttributeDefaults) {
793 nodeImpl[key] = nodeData[key];
795 nodeImpl[key] = AutomationAttributeDefaults[key];
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);
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);
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);
823 // If this is a link, set link attributes
824 if (nodeData.role == 'link') {
825 this.mixinAttributes_(nodeImpl, LinkMixinAttributes, nodeData);
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);
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);
841 // If this is a table, set table attributes.
842 if (nodeData.role == schema.RoleType.table) {
843 this.mixinAttributes_(nodeImpl, TableMixinAttributes, nodeData);
846 // If this is a table cell, set table cell attributes.
847 if (nodeData.role == schema.RoleType.cell) {
848 this.mixinAttributes_(nodeImpl, TableCellMixinAttributes, nodeData);
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);
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)) {
866 ATTRIBUTE_NAME_TO_ID_ATTRIBUTE.hasOwnProperty(attributeName)) {
867 this.defineReadonlyAttribute_(nodeImpl,
872 this.defineReadonlyAttribute_(nodeImpl,
880 mixinAttributes_: function(nodeImpl, attributes, nodeData) {
881 for (var attribute in attributes) {
882 var spec = attributes[attribute];
884 this.mixinRelationshipAttribute_(nodeImpl, attribute, spec, nodeData);
886 this.mixinAttribute_(nodeImpl, attribute, spec, nodeData);
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);
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;
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)
911 nodeImpl.attributesInternal[idAttribute] = idValue;
912 this.defineReadonlyAttribute_(
913 nodeImpl, nodeImpl, attribute, true, idAttribute);
916 findAttribute_: function(attribute, spec, nodeData) {
917 if (!('reflectFrom' in spec))
919 var attributeGroup = spec.reflectFrom;
920 if (!(attributeGroup in nodeData))
923 return nodeData[attributeGroup][attribute];
926 defineReadonlyAttribute_: function(
927 node, object, attributeName, opt_isIDRef, opt_idAttribute) {
928 if (attributeName in object)
932 $Object.defineProperty(object, attributeName, {
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);
943 return node.rootImpl.get(idValue);
947 $Object.defineProperty(object, attributeName, {
950 return node.attributesInternal[attributeName];
955 if (object instanceof AutomationNodeImpl) {
956 // Also expose attribute publicly on the wrapper.
957 $Object.defineProperty(object.wrapper, attributeName, {
960 return object[attributeName];
967 updateNode_: function(nodeData, updateState) {
968 var node = this.axNodeDataCache_[nodeData.id];
969 var didUpdateRoot = false;
971 delete updateState.pendingNodes[privates(node).impl.id];
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',
983 // |this| is an AutomationRootNodeImpl; retrieve the
984 // AutomationRootNode instance instead.
986 didUpdateRoot = true;
987 updateState.newNodes[this.id] = this.wrapper;
989 this.setData_(node, nodeData);
991 // TODO(aboxhall): send onChanged event?
993 if (!this.deleteOldChildren_(node, nodeData.childIds)) {
995 this.invalidate_(this.wrapper);
999 var nodeImpl = privates(node).impl;
1001 var success = this.createNewChildren_(node,
1004 nodeImpl.childIds = nodeData.childIds;
1005 this.axNodeDataCache_[nodeImpl.id] = node;
1012 var AutomationNode = utils.expose('AutomationNode',
1014 { functions: ['doDefault',
1022 'removeEventListener',
1023 'addTreeChangeObserver',
1024 'removeTreeChangeObserver',
1027 readonly: ['parent',
1041 var AutomationRootNode = utils.expose('AutomationRootNode',
1042 AutomationRootNodeImpl,
1043 { superclass: AutomationNode });
1045 exports.AutomationNode = AutomationNode;
1046 exports.AutomationRootNode = AutomationRootNode;