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 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);
142 return { treeID
: this.treeID
,
145 attributes
: this.attributes
};
148 dispatchEvent: function(eventType
) {
150 var parent
= this.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
;
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
)
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
)
207 fireEventListeners_: function(node
, event
) {
208 var nodeImpl
= privates(node
).impl
;
209 var listeners
= nodeImpl
.listeners
[event
.type
];
212 var eventPhase
= event
.eventPhase
;
213 for (var i
= 0; i
< listeners
.length
; i
++) {
214 if (eventPhase
== Event
.CAPTURING_PHASE
&& !listeners
[i
].capture
)
216 if (eventPhase
== Event
.BUBBLING_PHASE
&& listeners
[i
].capture
)
220 listeners
[i
].callback(event
);
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) {
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
},
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
252 if (!resultAutomationNodeID
) {
256 var resultNode
= this.rootImpl
.get(resultAutomationNodeID
);
258 logging
.WARNING('Query selector result not in tree: ' +
259 resultAutomationNodeID
);
262 userCallback(resultNode
);
265 findInternal_: function(params
, opt_results
) {
267 this.forAllDescendants_(function(node
) {
268 if (privates(node
).impl
.matchInternal_(params
)) {
270 opt_results
.push(node
);
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();
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)
304 if ('role' in params
&& this.role
!= params
.role
)
307 if ('state' in params
) {
308 for (var state
in params
.state
) {
309 if (params
.state
[state
] != (state
in this.state
))
313 if ('attributes' in params
) {
314 for (var attribute
in params
.attributes
) {
315 if (!(attribute
in this.attributesInternal
))
318 var attrValue
= params
.attributes
[attribute
];
319 if (typeof attrValue
!= 'object') {
320 if (this.attributesInternal
[attribute
] !== attrValue
)
322 } else if (attrValue
instanceof RegExp
) {
323 if (typeof this.attributesInternal
[attribute
] != 'string')
325 if (!attrValue
.test(this.attributesInternal
[attribute
]))
328 // TODO(aboxhall): handle intlist case.
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
= {
343 'location': { left
: 0, top
: 0, width
: 0, height
: 0 }
347 var AutomationAttributeTypes
= [
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>}
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>}
376 var ATTRIBUTE_BLACKLIST
= {'activedescendantId': true,
379 'describedbyIds': true,
381 'labelledbyIds': 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
,
422 function defaultNodeRefListAttribute(idAttribute
, opt_defaultVal
) {
423 var defaultVal
= (opt_defaultVal
!== undefined) ? opt_defaultVal
: [];
424 return { default: [],
425 idFrom
: 'intlistAttributes',
426 idAttribute
: idAttribute
,
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.
520 function AutomationRootNodeImpl(treeID
) {
521 AutomationNodeImpl
.call(this, this);
522 this.treeID
= treeID
;
523 this.axNodeDataCache_
= {};
526 AutomationRootNodeImpl
.prototype = {
527 __proto__
: AutomationNodeImpl
.prototype,
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',
550 } else if (update
.nodeIdToClear
> 0) {
551 var nodeToClear
= this.axNodeDataCache_
[update
.nodeIdToClear
];
553 logging
.WARNING('Bad nodeIdToClear: ' + update
.nodeIdToClear
+
555 lastError
.set('automation',
556 'Bad update received on automation tree',
561 if (nodeToClear
=== this.wrapper
) {
562 this.invalidate_(nodeToClear
);
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
))
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',
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
];
596 {target
: node
, type
: schema
.TreeChangeType
.nodeCreated
};
597 for (var i
= 0; i
< observers
.length
; i
++) {
599 observers
[i
](treeChange
);
601 console
.error('Error in tree change observer for ' +
602 treeChange
.type
+ ': ' + e
.message
+
603 '\nStack trace: ' + e
.stack
);
612 destroy: function() {
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');
627 var targetNode
= this.get(eventParams
.targetID
);
629 var targetNodeImpl
= privates(targetNode
).impl
;
630 targetNodeImpl
.dispatchEvent(eventParams
.eventType
);
632 logging
.WARNING('Got ' + eventParams
.eventType
+
633 ' event on unknown node: ' + eventParams
.targetID
+
634 '; this: ' + this.id
);
639 toString: function() {
640 function toStringInternal(node
, indent
) {
644 new Array(indent
).join(' ') +
645 AutomationNodeImpl
.prototype.toString
.call(node
) +
648 for (var i
= 0; i
< node
.children
.length
; i
++)
649 output
+= toStringInternal(node
.children
[i
], indent
);
652 return toStringInternal(this, 0);
655 invalidate_: function(node
) {
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
++) {
665 observers
[i
](treeChange
);
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
)
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
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
= [];
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',
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);
733 nodeImpl
.childIds
= oldChildIds
;
738 createNewChildren_: function(node
, newChildIds
, updateState
) {
742 for (var i
= 0; i
< newChildIds
.length
; i
++) {
743 var childId
= newChildIds
[i
];
744 var childNode
= this.axNodeDataCache_
[childId
];
746 if (childNode
.parent
!= node
) {
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',
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
;
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
) {
801 nodeImpl
[key
] = nodeData
[key
];
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
);
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
)) {
875 ATTRIBUTE_NAME_TO_ID_ATTRIBUTE
.hasOwnProperty(attributeName
)) {
876 this.defineReadonlyAttribute_(nodeImpl
,
881 this.defineReadonlyAttribute_(nodeImpl
,
889 mixinAttributes_: function(nodeImpl
, attributes
, nodeData
) {
890 for (var attribute
in attributes
) {
891 var spec
= attributes
[attribute
];
893 this.mixinRelationshipAttribute_(nodeImpl
, attribute
, spec
, nodeData
);
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)
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
))
928 var attributeGroup
= spec
.reflectFrom
;
929 if (!(attributeGroup
in nodeData
))
932 return nodeData
[attributeGroup
][attribute
];
935 defineReadonlyAttribute_: function(
936 node
, object
, attributeName
, opt_isIDRef
, opt_idAttribute
) {
937 if (attributeName
in object
)
941 $Object
.defineProperty(object
, attributeName
, {
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
);
952 return node
.rootImpl
.get(idValue
);
956 $Object
.defineProperty(object
, attributeName
, {
959 return node
.attributesInternal
[attributeName
];
964 if (object
instanceof AutomationNodeImpl
) {
965 // Also expose attribute publicly on the wrapper.
966 $Object
.defineProperty(object
.wrapper
, attributeName
, {
969 return object
[attributeName
];
976 updateNode_: function(nodeData
, updateState
) {
977 var node
= this.axNodeDataCache_
[nodeData
.id
];
978 var didUpdateRoot
= false;
980 delete updateState
.pendingNodes
[privates(node
).impl
.id
];
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',
992 // |this| is an AutomationRootNodeImpl; retrieve the
993 // AutomationRootNode instance instead.
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
);
1008 var nodeImpl
= privates(node
).impl
;
1010 var success
= this.createNewChildren_(node
,
1013 nodeImpl
.childIds
= nodeData
.childIds
;
1014 this.axNodeDataCache_
[nodeImpl
.id
] = node
;
1021 var AutomationNode
= utils
.expose('AutomationNode',
1023 { functions
: ['doDefault',
1032 'removeEventListener',
1035 readonly
: ['parent',
1049 var AutomationRootNode
= utils
.expose('AutomationRootNode',
1050 AutomationRootNodeImpl
,
1051 { superclass
: AutomationNode
});
1053 exports
.AutomationNode
= AutomationNode
;
1054 exports
.AutomationRootNode
= AutomationRootNode
;