Reland "Non-SFI mode: Switch to newlib. (patchset #4 id:60001 of https://codereview...
[chromium-blink-merge.git] / chrome / renderer / resources / extensions / automation / automation_node.js
blob11ac8d8a3b065b5e292df2df259d81716aa3c95f
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 var AutomationEvent = require('automationEvent').AutomationEvent;
6 var automationInternal =
7 require('binding').Binding.create('automationInternal').generate();
8 var IsInteractPermitted =
9 requireNative('automationInternal').IsInteractPermitted;
11 var lastError = require('lastError');
12 var logging = requireNative('logging');
13 var schema = requireNative('automationInternal').GetSchemaAdditions();
14 var utils = require('utils');
16 /**
17 * A single node in the Automation tree.
18 * @param {AutomationRootNodeImpl} root The root of the tree.
19 * @constructor
21 function AutomationNodeImpl(root) {
22 this.rootImpl = root;
23 this.childIds = [];
24 // Public attributes. No actual data gets set on this object.
25 this.attributes = {};
26 // Internal object holding all attributes.
27 this.attributesInternal = {};
28 this.listeners = {};
29 this.location = { left: 0, top: 0, width: 0, height: 0 };
32 AutomationNodeImpl.prototype = {
33 id: -1,
34 role: '',
35 state: { busy: true },
36 isRootNode: false,
38 get root() {
39 return this.rootImpl.wrapper;
42 get parent() {
43 return this.hostTree || this.rootImpl.get(this.parentID);
46 get firstChild() {
47 return this.childTree || this.rootImpl.get(this.childIds[0]);
50 get lastChild() {
51 var childIds = this.childIds;
52 return this.childTree || this.rootImpl.get(childIds[childIds.length - 1]);
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));
64 return children;
67 get previousSibling() {
68 var parent = this.parent;
69 if (parent && this.indexInParent > 0)
70 return parent.children[this.indexInParent - 1];
71 return undefined;
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;
81 doDefault: function() {
82 this.performAction_('doDefault');
85 focus: function() {
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 showContextMenu: function() {
100 this.performAction_('showContextMenu');
103 domQuerySelector: function(selector, callback) {
104 automationInternal.querySelector(
105 { treeID: this.rootImpl.treeID,
106 automationNodeID: this.id,
107 selector: selector },
108 this.domQuerySelectorCallback_.bind(this, callback));
111 find: function(params) {
112 return this.findInternal_(params);
115 findAll: function(params) {
116 return this.findInternal_(params, []);
119 matches: function(params) {
120 return this.matchInternal_(params);
123 addEventListener: function(eventType, callback, capture) {
124 this.removeEventListener(eventType, callback);
125 if (!this.listeners[eventType])
126 this.listeners[eventType] = [];
127 this.listeners[eventType].push({callback: callback, capture: !!capture});
130 // TODO(dtseng/aboxhall): Check this impl against spec.
131 removeEventListener: function(eventType, callback) {
132 if (this.listeners[eventType]) {
133 var listeners = this.listeners[eventType];
134 for (var i = 0; i < listeners.length; i++) {
135 if (callback === listeners[i].callback)
136 listeners.splice(i, 1);
141 toJSON: function() {
142 return { treeID: this.treeID,
143 id: this.id,
144 role: this.role,
145 attributes: this.attributes };
148 dispatchEvent: function(eventType) {
149 var path = [];
150 var parent = this.parent;
151 while (parent) {
152 path.push(parent);
153 parent = parent.parent;
155 var event = new AutomationEvent(eventType, this.wrapper);
157 // Dispatch the event through the propagation path in three phases:
158 // - capturing: starting from the root and going down to the target's parent
159 // - targeting: dispatching the event on the target itself
160 // - bubbling: starting from the target's parent, going back up to the root.
161 // At any stage, a listener may call stopPropagation() on the event, which
162 // will immediately stop event propagation through this path.
163 if (this.dispatchEventAtCapturing_(event, path)) {
164 if (this.dispatchEventAtTargeting_(event, path))
165 this.dispatchEventAtBubbling_(event, path);
169 toString: function() {
170 var impl = privates(this).impl;
171 if (!impl)
172 impl = this;
173 return 'node id=' + impl.id +
174 ' role=' + this.role +
175 ' state=' + $JSON.stringify(this.state) +
176 ' parentID=' + impl.parentID +
177 ' childIds=' + $JSON.stringify(impl.childIds) +
178 ' attributes=' + $JSON.stringify(this.attributes);
181 dispatchEventAtCapturing_: function(event, path) {
182 privates(event).impl.eventPhase = Event.CAPTURING_PHASE;
183 for (var i = path.length - 1; i >= 0; i--) {
184 this.fireEventListeners_(path[i], event);
185 if (privates(event).impl.propagationStopped)
186 return false;
188 return true;
191 dispatchEventAtTargeting_: function(event) {
192 privates(event).impl.eventPhase = Event.AT_TARGET;
193 this.fireEventListeners_(this.wrapper, event);
194 return !privates(event).impl.propagationStopped;
197 dispatchEventAtBubbling_: function(event, path) {
198 privates(event).impl.eventPhase = Event.BUBBLING_PHASE;
199 for (var i = 0; i < path.length; i++) {
200 this.fireEventListeners_(path[i], event);
201 if (privates(event).impl.propagationStopped)
202 return false;
204 return true;
207 fireEventListeners_: function(node, event) {
208 var nodeImpl = privates(node).impl;
209 var listeners = nodeImpl.listeners[event.type];
210 if (!listeners)
211 return;
212 var eventPhase = event.eventPhase;
213 for (var i = 0; i < listeners.length; i++) {
214 if (eventPhase == Event.CAPTURING_PHASE && !listeners[i].capture)
215 continue;
216 if (eventPhase == Event.BUBBLING_PHASE && listeners[i].capture)
217 continue;
219 try {
220 listeners[i].callback(event);
221 } catch (e) {
222 console.error('Error in event handler for ' + event.type +
223 'during phase ' + eventPhase + ': ' +
224 e.message + '\nStack trace: ' + e.stack);
229 performAction_: function(actionType, opt_args) {
230 // Not yet initialized.
231 if (this.rootImpl.treeID === undefined ||
232 this.id === undefined) {
233 return;
236 // Check permissions.
237 if (!IsInteractPermitted()) {
238 throw new Error(actionType + ' requires {"desktop": true} or' +
239 ' {"interact": true} in the "automation" manifest key.');
242 automationInternal.performAction({ treeID: this.rootImpl.treeID,
243 automationNodeID: this.id,
244 actionType: actionType },
245 opt_args || {});
248 domQuerySelectorCallback_: function(userCallback, resultAutomationNodeID) {
249 // resultAutomationNodeID could be zero or undefined or (unlikely) null;
250 // they all amount to the same thing here, which is that no node was
251 // returned.
252 if (!resultAutomationNodeID) {
253 userCallback(null);
254 return;
256 var resultNode = this.rootImpl.get(resultAutomationNodeID);
257 if (!resultNode) {
258 logging.WARNING('Query selector result not in tree: ' +
259 resultAutomationNodeID);
260 userCallback(null);
262 userCallback(resultNode);
265 findInternal_: function(params, opt_results) {
266 var result = null;
267 this.forAllDescendants_(function(node) {
268 if (privates(node).impl.matchInternal_(params)) {
269 if (opt_results)
270 opt_results.push(node);
271 else
272 result = node;
273 return !opt_results;
276 if (opt_results)
277 return opt_results;
278 return result;
282 * Executes a closure for all of this node's descendants, in pre-order.
283 * Early-outs if the closure returns true.
284 * @param {Function(AutomationNode):boolean} closure Closure to be executed
285 * for each node. Return true to early-out the traversal.
287 forAllDescendants_: function(closure) {
288 var stack = this.wrapper.children.reverse();
289 while (stack.length > 0) {
290 var node = stack.pop();
291 if (closure(node))
292 return;
294 var children = node.children;
295 for (var i = children.length - 1; i >= 0; i--)
296 stack.push(children[i]);
300 matchInternal_: function(params) {
301 if (Object.keys(params).length == 0)
302 return false;
304 if ('role' in params && this.role != params.role)
305 return false;
307 if ('state' in params) {
308 for (var state in params.state) {
309 if (params.state[state] != (state in this.state))
310 return false;
313 if ('attributes' in params) {
314 for (var attribute in params.attributes) {
315 if (!(attribute in this.attributesInternal))
316 return false;
318 var attrValue = params.attributes[attribute];
319 if (typeof attrValue != 'object') {
320 if (this.attributesInternal[attribute] !== attrValue)
321 return false;
322 } else if (attrValue instanceof RegExp) {
323 if (typeof this.attributesInternal[attribute] != 'string')
324 return false;
325 if (!attrValue.test(this.attributesInternal[attribute]))
326 return false;
327 } else {
328 // TODO(aboxhall): handle intlist case.
329 return false;
333 return true;
337 // Maps an attribute to its default value in an invalidated node.
338 // These attributes are taken directly from the Automation idl.
339 var AutomationAttributeDefaults = {
340 'id': -1,
341 'role': '',
342 'state': {},
343 'location': { left: 0, top: 0, width: 0, height: 0 }
347 var AutomationAttributeTypes = [
348 'boolAttributes',
349 'floatAttributes',
350 'htmlAttributes',
351 'intAttributes',
352 'intlistAttributes',
353 'stringAttributes'
357 * Maps an attribute name to another attribute who's value is an id or an array
358 * of ids referencing an AutomationNode.
359 * @param {!Object<string>}
360 * @const
362 var ATTRIBUTE_NAME_TO_ID_ATTRIBUTE = {
363 'aria-activedescendant': 'activedescendantId',
364 'aria-controls': 'controlsIds',
365 'aria-describedby': 'describedbyIds',
366 'aria-flowto': 'flowtoIds',
367 'aria-labelledby': 'labelledbyIds',
368 'aria-owns': 'ownsIds'
372 * A set of attributes ignored in the automation API.
373 * @param {!Object<boolean>}
374 * @const
376 var ATTRIBUTE_BLACKLIST = {'activedescendantId': true,
377 'childTreeId': true,
378 'controlsIds': true,
379 'describedbyIds': true,
380 'flowtoIds': true,
381 'labelledbyIds': true,
382 'ownsIds': true
385 function defaultStringAttribute(opt_defaultVal) {
386 return { default: undefined, reflectFrom: 'stringAttributes' };
389 function defaultIntAttribute(opt_defaultVal) {
390 var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : 0;
391 return { default: defaultVal, reflectFrom: 'intAttributes' };
394 function defaultFloatAttribute(opt_defaultVal) {
395 var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : 0;
396 return { default: defaultVal, reflectFrom: 'floatAttributes' };
399 function defaultBoolAttribute(opt_defaultVal) {
400 var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : false;
401 return { default: defaultVal, reflectFrom: 'boolAttributes' };
404 function defaultHtmlAttribute(opt_defaultVal) {
405 var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : '';
406 return { default: defaultVal, reflectFrom: 'htmlAttributes' };
409 function defaultIntListAttribute(opt_defaultVal) {
410 var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : [];
411 return { default: defaultVal, reflectFrom: 'intlistAttributes' };
414 function defaultNodeRefAttribute(idAttribute, opt_defaultVal) {
415 var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : null;
416 return { default: defaultVal,
417 idFrom: 'intAttributes',
418 idAttribute: idAttribute,
419 isRef: true };
422 function defaultNodeRefListAttribute(idAttribute, opt_defaultVal) {
423 var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : [];
424 return { default: [],
425 idFrom: 'intlistAttributes',
426 idAttribute: idAttribute,
427 isRef: true };
430 // Maps an attribute to its default value in an invalidated node.
431 // These attributes are taken directly from the Automation idl.
432 var DefaultMixinAttributes = {
433 description: defaultStringAttribute(),
434 help: defaultStringAttribute(),
435 name: defaultStringAttribute(),
436 value: defaultStringAttribute(),
437 htmlTag: defaultStringAttribute(),
438 hierarchicalLevel: defaultIntAttribute(),
439 controls: defaultNodeRefListAttribute('controlsIds'),
440 describedby: defaultNodeRefListAttribute('describedbyIds'),
441 flowto: defaultNodeRefListAttribute('flowtoIds'),
442 labelledby: defaultNodeRefListAttribute('labelledbyIds'),
443 owns: defaultNodeRefListAttribute('ownsIds'),
444 wordStarts: defaultIntListAttribute(),
445 wordEnds: defaultIntListAttribute()
448 var ActiveDescendantMixinAttribute = {
449 activedescendant: defaultNodeRefAttribute('activedescendantId')
452 var LinkMixinAttributes = {
453 url: defaultStringAttribute()
456 var DocumentMixinAttributes = {
457 docUrl: defaultStringAttribute(),
458 docTitle: defaultStringAttribute(),
459 docLoaded: defaultStringAttribute(),
460 docLoadingProgress: defaultFloatAttribute()
463 var ScrollableMixinAttributes = {
464 scrollX: defaultIntAttribute(),
465 scrollXMin: defaultIntAttribute(),
466 scrollXMax: defaultIntAttribute(),
467 scrollY: defaultIntAttribute(),
468 scrollYMin: defaultIntAttribute(),
469 scrollYMax: defaultIntAttribute()
472 var EditableTextMixinAttributes = {
473 textSelStart: defaultIntAttribute(-1),
474 textSelEnd: defaultIntAttribute(-1),
475 type: defaultHtmlAttribute()
478 var RangeMixinAttributes = {
479 valueForRange: defaultFloatAttribute(),
480 minValueForRange: defaultFloatAttribute(),
481 maxValueForRange: defaultFloatAttribute()
484 var TableMixinAttributes = {
485 tableRowCount: defaultIntAttribute(),
486 tableColumnCount: defaultIntAttribute()
489 var TableCellMixinAttributes = {
490 tableCellColumnIndex: defaultIntAttribute(),
491 tableCellColumnSpan: defaultIntAttribute(1),
492 tableCellRowIndex: defaultIntAttribute(),
493 tableCellRowSpan: defaultIntAttribute(1)
496 var LiveRegionMixinAttributes = {
497 containerLiveAtomic: defaultBoolAttribute(),
498 containerLiveBusy: defaultBoolAttribute(),
499 containerLiveRelevant: defaultStringAttribute(),
500 containerLiveStatus: defaultStringAttribute(),
504 * AutomationRootNode.
506 * An AutomationRootNode is the javascript end of an AXTree living in the
507 * browser. AutomationRootNode handles unserializing incremental updates from
508 * the source AXTree. Each update contains node data that form a complete tree
509 * after applying the update.
511 * A brief note about ids used through this class. The source AXTree assigns
512 * unique ids per node and we use these ids to build a hash to the actual
513 * AutomationNode object.
514 * Thus, tree traversals amount to a lookup in our hash.
516 * The tree itself is identified by the accessibility tree id of the
517 * renderer widget host.
518 * @constructor
520 function AutomationRootNodeImpl(treeID) {
521 AutomationNodeImpl.call(this, this);
522 this.treeID = treeID;
523 this.axNodeDataCache_ = {};
526 AutomationRootNodeImpl.prototype = {
527 __proto__: AutomationNodeImpl.prototype,
529 isRootNode: true,
530 treeID: -1,
532 get: function(id) {
533 if (id == undefined)
534 return undefined;
536 return this.axNodeDataCache_[id];
539 unserialize: function(update) {
540 var updateState = { pendingNodes: {}, newNodes: {} };
541 var oldRootId = this.id;
543 if (update.nodeIdToClear < 0) {
544 logging.WARNING('Bad nodeIdToClear: ' + update.nodeIdToClear);
545 lastError.set('automation',
546 'Bad update received on automation tree',
547 null,
548 chrome);
549 return false;
550 } else if (update.nodeIdToClear > 0) {
551 var nodeToClear = this.axNodeDataCache_[update.nodeIdToClear];
552 if (!nodeToClear) {
553 logging.WARNING('Bad nodeIdToClear: ' + update.nodeIdToClear +
554 ' (not in cache)');
555 lastError.set('automation',
556 'Bad update received on automation tree',
557 null,
558 chrome);
559 return false;
561 if (nodeToClear === this.wrapper) {
562 this.invalidate_(nodeToClear);
563 } else {
564 var children = nodeToClear.children;
565 for (var i = 0; i < children.length; i++)
566 this.invalidate_(children[i]);
567 var nodeToClearImpl = privates(nodeToClear).impl;
568 nodeToClearImpl.childIds = []
569 updateState.pendingNodes[nodeToClearImpl.id] = nodeToClear;
573 for (var i = 0; i < update.nodes.length; i++) {
574 if (!this.updateNode_(update.nodes[i], updateState))
575 return false;
578 if (Object.keys(updateState.pendingNodes).length > 0) {
579 logging.WARNING('Nodes left pending by the update: ' +
580 $JSON.stringify(updateState.pendingNodes));
581 lastError.set('automation',
582 'Bad update received on automation tree',
583 null,
584 chrome);
585 return false;
588 // Notify tree change observers of new nodes.
589 // TODO(dmazzoni): Notify tree change observers of changed nodes,
590 // and handle subtreeCreated and nodeCreated properly.
591 var observers = automationUtil.treeChangeObservers;
592 if (observers.length > 0) {
593 for (var nodeId in updateState.newNodes) {
594 var node = updateState.newNodes[nodeId];
595 var treeChange =
596 {target: node, type: schema.TreeChangeType.nodeCreated};
597 for (var i = 0; i < observers.length; i++) {
598 try {
599 observers[i](treeChange);
600 } catch (e) {
601 console.error('Error in tree change observer for ' +
602 treeChange.type + ': ' + e.message +
603 '\nStack trace: ' + e.stack);
609 return true;
612 destroy: function() {
613 if (this.hostTree)
614 this.hostTree.childTree = undefined;
615 this.hostTree = undefined;
617 this.dispatchEvent(schema.EventType.destroyed);
618 this.invalidate_(this.wrapper);
621 onAccessibilityEvent: function(eventParams) {
622 if (!this.unserialize(eventParams.update)) {
623 logging.WARNING('unserialization failed');
624 return false;
627 var targetNode = this.get(eventParams.targetID);
628 if (targetNode) {
629 var targetNodeImpl = privates(targetNode).impl;
630 targetNodeImpl.dispatchEvent(eventParams.eventType);
631 } else {
632 logging.WARNING('Got ' + eventParams.eventType +
633 ' event on unknown node: ' + eventParams.targetID +
634 '; this: ' + this.id);
636 return true;
639 toString: function() {
640 function toStringInternal(node, indent) {
641 if (!node)
642 return '';
643 var output =
644 new Array(indent).join(' ') +
645 AutomationNodeImpl.prototype.toString.call(node) +
646 '\n';
647 indent += 2;
648 for (var i = 0; i < node.children.length; i++)
649 output += toStringInternal(node.children[i], indent);
650 return output;
652 return toStringInternal(this, 0);
655 invalidate_: function(node) {
656 if (!node)
657 return;
659 // Notify tree change observers of the removed node.
660 var observers = automationUtil.treeChangeObservers;
661 if (observers.length > 0) {
662 var treeChange = {target: node, type: schema.TreeChangeType.nodeRemoved};
663 for (var i = 0; i < observers.length; i++) {
664 try {
665 observers[i](treeChange);
666 } catch (e) {
667 console.error('Error in tree change observer for ' + treeChange.type +
668 ': ' + e.message + '\nStack trace: ' + e.stack);
673 var children = node.children;
675 for (var i = 0, child; child = children[i]; i++) {
676 // Do not invalidate into subrooted nodes.
677 // TODO(dtseng): Revisit logic once out of proc iframes land.
678 if (child.root != node.root)
679 continue;
680 this.invalidate_(child);
683 // Retrieve the internal AutomationNodeImpl instance for this node.
684 // This object is not accessible outside of bindings code, but we can access
685 // it here.
686 var nodeImpl = privates(node).impl;
687 var id = nodeImpl.id;
688 for (var key in AutomationAttributeDefaults) {
689 nodeImpl[key] = AutomationAttributeDefaults[key];
692 nodeImpl.attributesInternal = {};
693 for (var key in DefaultMixinAttributes) {
694 var mixinAttribute = DefaultMixinAttributes[key];
695 if (!mixinAttribute.isRef)
696 nodeImpl.attributesInternal[key] = mixinAttribute.default;
698 nodeImpl.childIds = [];
699 nodeImpl.id = id;
700 delete this.axNodeDataCache_[id];
703 deleteOldChildren_: function(node, newChildIds) {
704 // Create a set of child ids in |src| for fast lookup, and return false
705 // if a duplicate is found;
706 var newChildIdSet = {};
707 for (var i = 0; i < newChildIds.length; i++) {
708 var childId = newChildIds[i];
709 if (newChildIdSet[childId]) {
710 logging.WARNING('Node ' + privates(node).impl.id +
711 ' has duplicate child id ' + childId);
712 lastError.set('automation',
713 'Bad update received on automation tree',
714 null,
715 chrome);
716 return false;
718 newChildIdSet[newChildIds[i]] = true;
721 // Delete the old children.
722 var nodeImpl = privates(node).impl;
723 var oldChildIds = nodeImpl.childIds;
724 for (var i = 0; i < oldChildIds.length;) {
725 var oldId = oldChildIds[i];
726 if (!newChildIdSet[oldId]) {
727 this.invalidate_(this.axNodeDataCache_[oldId]);
728 oldChildIds.splice(i, 1);
729 } else {
730 i++;
733 nodeImpl.childIds = oldChildIds;
735 return true;
738 createNewChildren_: function(node, newChildIds, updateState) {
739 logging.CHECK(node);
740 var success = true;
742 for (var i = 0; i < newChildIds.length; i++) {
743 var childId = newChildIds[i];
744 var childNode = this.axNodeDataCache_[childId];
745 if (childNode) {
746 if (childNode.parent != node) {
747 var parentId = -1;
748 if (childNode.parent) {
749 var parentImpl = privates(childNode.parent).impl;
750 parentId = parentImpl.id;
752 // This is a serious error - nodes should never be reparented.
753 // If this case occurs, continue so this node isn't left in an
754 // inconsistent state, but return failure at the end.
755 logging.WARNING('Node ' + childId + ' reparented from ' +
756 parentId + ' to ' + privates(node).impl.id);
757 lastError.set('automation',
758 'Bad update received on automation tree',
759 null,
760 chrome);
761 success = false;
762 continue;
764 } else {
765 childNode = new AutomationNode(this);
766 this.axNodeDataCache_[childId] = childNode;
767 privates(childNode).impl.id = childId;
768 updateState.pendingNodes[childId] = childNode;
769 updateState.newNodes[childId] = childNode;
771 privates(childNode).impl.indexInParent = i;
772 privates(childNode).impl.parentID = privates(node).impl.id;
775 return success;
778 setData_: function(node, nodeData) {
779 var nodeImpl = privates(node).impl;
781 // TODO(dtseng): Make into set listing all hosting node roles.
782 if (nodeData.role == schema.RoleType.webView) {
783 if (nodeImpl.childTreeID !== nodeData.intAttributes.childTreeId)
784 nodeImpl.pendingChildFrame = true;
786 if (nodeImpl.pendingChildFrame) {
787 nodeImpl.childTreeID = nodeData.intAttributes.childTreeId;
788 automationUtil.storeTreeCallback(nodeImpl.childTreeID, function(root) {
789 nodeImpl.pendingChildFrame = false;
790 nodeImpl.childTree = root;
791 privates(root).impl.hostTree = node;
792 if (root.attributes.docLoadingProgress == 1)
793 privates(root).impl.dispatchEvent(schema.EventType.loadComplete);
794 nodeImpl.dispatchEvent(schema.EventType.childrenChanged);
796 automationInternal.enableFrame(nodeImpl.childTreeID);
799 for (var key in AutomationAttributeDefaults) {
800 if (key in nodeData)
801 nodeImpl[key] = nodeData[key];
802 else
803 nodeImpl[key] = AutomationAttributeDefaults[key];
806 // Set all basic attributes.
807 this.mixinAttributes_(nodeImpl, DefaultMixinAttributes, nodeData);
809 // If this is a rootWebArea or webArea, set document attributes.
810 if (nodeData.role == schema.RoleType.rootWebArea ||
811 nodeData.role == schema.RoleType.webArea) {
812 this.mixinAttributes_(nodeImpl, DocumentMixinAttributes, nodeData);
815 // If this is a scrollable area, set scrollable attributes.
816 for (var scrollAttr in ScrollableMixinAttributes) {
817 var spec = ScrollableMixinAttributes[scrollAttr];
818 if (this.findAttribute_(scrollAttr, spec, nodeData) !== undefined) {
819 this.mixinAttributes_(nodeImpl, ScrollableMixinAttributes, nodeData);
820 break;
824 // If this is inside a live region, set live region mixins.
825 var attr = 'containerLiveStatus';
826 var spec = LiveRegionMixinAttributes[attr];
827 if (this.findAttribute_(attr, spec, nodeData) !== undefined) {
828 this.mixinAttributes_(nodeImpl, LiveRegionMixinAttributes, nodeData);
831 // If this is a link, set link attributes
832 if (nodeData.role == 'link') {
833 this.mixinAttributes_(nodeImpl, LinkMixinAttributes, nodeData);
836 // If this is an editable text area, set editable text attributes.
837 if (nodeData.role == schema.RoleType.textField ||
838 nodeData.role == schema.RoleType.spinButton) {
839 this.mixinAttributes_(nodeImpl, EditableTextMixinAttributes, nodeData);
842 // If this is a range type, set range attributes.
843 if (nodeData.role == schema.RoleType.progressIndicator ||
844 nodeData.role == schema.RoleType.scrollBar ||
845 nodeData.role == schema.RoleType.slider ||
846 nodeData.role == schema.RoleType.spinButton) {
847 this.mixinAttributes_(nodeImpl, RangeMixinAttributes, nodeData);
850 // If this is a table, set table attributes.
851 if (nodeData.role == schema.RoleType.table) {
852 this.mixinAttributes_(nodeImpl, TableMixinAttributes, nodeData);
855 // If this is a table cell, set table cell attributes.
856 if (nodeData.role == schema.RoleType.cell) {
857 this.mixinAttributes_(nodeImpl, TableCellMixinAttributes, nodeData);
860 // If this has an active descendant, expose it.
861 if ('intAttributes' in nodeData &&
862 'activedescendantId' in nodeData.intAttributes) {
863 this.mixinAttributes_(nodeImpl, ActiveDescendantMixinAttribute, nodeData);
866 for (var i = 0; i < AutomationAttributeTypes.length; i++) {
867 var attributeType = AutomationAttributeTypes[i];
868 for (var attributeName in nodeData[attributeType]) {
869 nodeImpl.attributesInternal[attributeName] =
870 nodeData[attributeType][attributeName];
871 if (ATTRIBUTE_BLACKLIST.hasOwnProperty(attributeName) ||
872 nodeImpl.attributes.hasOwnProperty(attributeName)) {
873 continue;
874 } else if (
875 ATTRIBUTE_NAME_TO_ID_ATTRIBUTE.hasOwnProperty(attributeName)) {
876 this.defineReadonlyAttribute_(nodeImpl,
877 nodeImpl.attributes,
878 attributeName,
879 true);
880 } else {
881 this.defineReadonlyAttribute_(nodeImpl,
882 nodeImpl.attributes,
883 attributeName);
889 mixinAttributes_: function(nodeImpl, attributes, nodeData) {
890 for (var attribute in attributes) {
891 var spec = attributes[attribute];
892 if (spec.isRef)
893 this.mixinRelationshipAttribute_(nodeImpl, attribute, spec, nodeData);
894 else
895 this.mixinAttribute_(nodeImpl, attribute, spec, nodeData);
899 mixinAttribute_: function(nodeImpl, attribute, spec, nodeData) {
900 var value = this.findAttribute_(attribute, spec, nodeData);
901 if (value === undefined)
902 value = spec.default;
903 nodeImpl.attributesInternal[attribute] = value;
904 this.defineReadonlyAttribute_(nodeImpl, nodeImpl, attribute);
907 mixinRelationshipAttribute_: function(nodeImpl, attribute, spec, nodeData) {
908 var idAttribute = spec.idAttribute;
909 var idValue = spec.default;
910 if (spec.idFrom in nodeData) {
911 idValue = idAttribute in nodeData[spec.idFrom]
912 ? nodeData[spec.idFrom][idAttribute] : idValue;
915 // Ok to define a list attribute with an empty list, but not a
916 // single ref with a null ID.
917 if (idValue === null)
918 return;
920 nodeImpl.attributesInternal[idAttribute] = idValue;
921 this.defineReadonlyAttribute_(
922 nodeImpl, nodeImpl, attribute, true, idAttribute);
925 findAttribute_: function(attribute, spec, nodeData) {
926 if (!('reflectFrom' in spec))
927 return;
928 var attributeGroup = spec.reflectFrom;
929 if (!(attributeGroup in nodeData))
930 return;
932 return nodeData[attributeGroup][attribute];
935 defineReadonlyAttribute_: function(
936 node, object, attributeName, opt_isIDRef, opt_idAttribute) {
937 if (attributeName in object)
938 return;
940 if (opt_isIDRef) {
941 $Object.defineProperty(object, attributeName, {
942 enumerable: true,
943 get: function() {
944 var idAttribute = opt_idAttribute ||
945 ATTRIBUTE_NAME_TO_ID_ATTRIBUTE[attributeName];
946 var idValue = node.attributesInternal[idAttribute];
947 if (Array.isArray(idValue)) {
948 return idValue.map(function(current) {
949 return node.rootImpl.get(current);
950 }, this);
952 return node.rootImpl.get(idValue);
953 }.bind(this),
955 } else {
956 $Object.defineProperty(object, attributeName, {
957 enumerable: true,
958 get: function() {
959 return node.attributesInternal[attributeName];
960 }.bind(this),
964 if (object instanceof AutomationNodeImpl) {
965 // Also expose attribute publicly on the wrapper.
966 $Object.defineProperty(object.wrapper, attributeName, {
967 enumerable: true,
968 get: function() {
969 return object[attributeName];
976 updateNode_: function(nodeData, updateState) {
977 var node = this.axNodeDataCache_[nodeData.id];
978 var didUpdateRoot = false;
979 if (node) {
980 delete updateState.pendingNodes[privates(node).impl.id];
981 } else {
982 if (nodeData.role != schema.RoleType.rootWebArea &&
983 nodeData.role != schema.RoleType.desktop) {
984 logging.WARNING(String(nodeData.id) +
985 ' is not in the cache and not the new root.');
986 lastError.set('automation',
987 'Bad update received on automation tree',
988 null,
989 chrome);
990 return false;
992 // |this| is an AutomationRootNodeImpl; retrieve the
993 // AutomationRootNode instance instead.
994 node = this.wrapper;
995 didUpdateRoot = true;
996 updateState.newNodes[this.id] = this.wrapper;
998 this.setData_(node, nodeData);
1000 // TODO(aboxhall): send onChanged event?
1001 logging.CHECK(node);
1002 if (!this.deleteOldChildren_(node, nodeData.childIds)) {
1003 if (didUpdateRoot) {
1004 this.invalidate_(this.wrapper);
1006 return false;
1008 var nodeImpl = privates(node).impl;
1010 var success = this.createNewChildren_(node,
1011 nodeData.childIds,
1012 updateState);
1013 nodeImpl.childIds = nodeData.childIds;
1014 this.axNodeDataCache_[nodeImpl.id] = node;
1016 return success;
1021 var AutomationNode = utils.expose('AutomationNode',
1022 AutomationNodeImpl,
1023 { functions: ['doDefault',
1024 'find',
1025 'findAll',
1026 'focus',
1027 'makeVisible',
1028 'matches',
1029 'setSelection',
1030 'showContextMenu',
1031 'addEventListener',
1032 'removeEventListener',
1033 'domQuerySelector',
1034 'toString' ],
1035 readonly: ['parent',
1036 'firstChild',
1037 'lastChild',
1038 'children',
1039 'previousSibling',
1040 'nextSibling',
1041 'isRootNode',
1042 'role',
1043 'state',
1044 'location',
1045 'attributes',
1046 'indexInParent',
1047 'root'] });
1049 var AutomationRootNode = utils.expose('AutomationRootNode',
1050 AutomationRootNodeImpl,
1051 { superclass: AutomationNode });
1053 exports.AutomationNode = AutomationNode;
1054 exports.AutomationRootNode = AutomationRootNode;