Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / chrome / renderer / resources / extensions / automation / automation_node.js
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.
4
5 var AutomationEvent = require('automationEvent').AutomationEvent;
6 var automationInternal =
7     require('binding').Binding.create('automationInternal').generate();
8 var IsInteractPermitted =
9     requireNative('automationInternal').IsInteractPermitted;
10
11 var lastError = require('lastError');
12 var logging = requireNative('logging');
13 var schema = requireNative('automationInternal').GetSchemaAdditions();
14 var utils = require('utils');
15
16 /**
17  * A single node in the Automation tree.
18  * @param {AutomationRootNodeImpl} root The root of the tree.
19  * @constructor
20  */
21 function AutomationNodeImpl(root) {
22   this.rootImpl = root;
23   this.childIds = [];
24   // Public attributes. No actual data gets set on this object.
25   this.attributes = {};
26   // Internal object holding all attributes.
27   this.attributesInternal = {};
28   this.listeners = {};
29   this.location = { left: 0, top: 0, width: 0, height: 0 };
30 }
31
32 AutomationNodeImpl.prototype = {
33   id: -1,
34   role: '',
35   state: { busy: true },
36   isRootNode: false,
37
38   get root() {
39     return this.rootImpl.wrapper;
40   },
41
42   parent: function() {
43     return this.hostTree || this.rootImpl.get(this.parentID);
44   },
45
46   firstChild: function() {
47     return this.childTree || this.rootImpl.get(this.childIds[0]);
48   },
49
50   lastChild: function() {
51     var childIds = this.childIds;
52     return this.childTree || this.rootImpl.get(childIds[childIds.length - 1]);
53   },
54
55   children: function() {
56     var children = [];
57     for (var i = 0, childID; childID = this.childIds[i]; i++) {
58       logging.CHECK(this.rootImpl.get(childID));
59       children.push(this.rootImpl.get(childID));
60     }
61     return children;
62   },
63
64   previousSibling: function() {
65     var parent = this.parent();
66     if (parent && this.indexInParent > 0)
67       return parent.children()[this.indexInParent - 1];
68     return undefined;
69   },
70
71   nextSibling: function() {
72     var parent = this.parent();
73     if (parent && this.indexInParent < parent.children().length)
74       return parent.children()[this.indexInParent + 1];
75     return undefined;
76   },
77
78   doDefault: function() {
79     this.performAction_('doDefault');
80   },
81
82   focus: function() {
83     this.performAction_('focus');
84   },
85
86   makeVisible: function() {
87     this.performAction_('makeVisible');
88   },
89
90   setSelection: function(startIndex, endIndex) {
91     this.performAction_('setSelection',
92                         { startIndex: startIndex,
93                           endIndex: endIndex });
94   },
95
96   querySelector: function(selector, callback) {
97     automationInternal.querySelector(
98       { treeID: this.rootImpl.treeID,
99         automationNodeID: this.id,
100         selector: selector },
101       this.querySelectorCallback_.bind(this, callback));
102   },
103
104   addEventListener: function(eventType, callback, capture) {
105     this.removeEventListener(eventType, callback);
106     if (!this.listeners[eventType])
107       this.listeners[eventType] = [];
108     this.listeners[eventType].push({callback: callback, capture: !!capture});
109   },
110
111   // TODO(dtseng/aboxhall): Check this impl against spec.
112   removeEventListener: function(eventType, callback) {
113     if (this.listeners[eventType]) {
114       var listeners = this.listeners[eventType];
115       for (var i = 0; i < listeners.length; i++) {
116         if (callback === listeners[i].callback)
117           listeners.splice(i, 1);
118       }
119     }
120   },
121
122   toJSON: function() {
123     return { treeID: this.treeID,
124              id: this.id,
125              role: this.role,
126              attributes: this.attributes };
127   },
128
129   dispatchEvent: function(eventType) {
130     var path = [];
131     var parent = this.parent();
132     while (parent) {
133       path.push(parent);
134       parent = parent.parent();
135     }
136     var event = new AutomationEvent(eventType, this.wrapper);
137
138     // Dispatch the event through the propagation path in three phases:
139     // - capturing: starting from the root and going down to the target's parent
140     // - targeting: dispatching the event on the target itself
141     // - bubbling: starting from the target's parent, going back up to the root.
142     // At any stage, a listener may call stopPropagation() on the event, which
143     // will immediately stop event propagation through this path.
144     if (this.dispatchEventAtCapturing_(event, path)) {
145       if (this.dispatchEventAtTargeting_(event, path))
146         this.dispatchEventAtBubbling_(event, path);
147     }
148   },
149
150   toString: function() {
151     var impl = privates(this).impl;
152     if (!impl)
153       impl = this;
154     return 'node id=' + impl.id +
155         ' role=' + this.role +
156         ' state=' + $JSON.stringify(this.state) +
157         ' parentID=' + impl.parentID +
158         ' childIds=' + $JSON.stringify(impl.childIds) +
159         ' attributes=' + $JSON.stringify(this.attributes);
160   },
161
162   dispatchEventAtCapturing_: function(event, path) {
163     privates(event).impl.eventPhase = Event.CAPTURING_PHASE;
164     for (var i = path.length - 1; i >= 0; i--) {
165       this.fireEventListeners_(path[i], event);
166       if (privates(event).impl.propagationStopped)
167         return false;
168     }
169     return true;
170   },
171
172   dispatchEventAtTargeting_: function(event) {
173     privates(event).impl.eventPhase = Event.AT_TARGET;
174     this.fireEventListeners_(this.wrapper, event);
175     return !privates(event).impl.propagationStopped;
176   },
177
178   dispatchEventAtBubbling_: function(event, path) {
179     privates(event).impl.eventPhase = Event.BUBBLING_PHASE;
180     for (var i = 0; i < path.length; i++) {
181       this.fireEventListeners_(path[i], event);
182       if (privates(event).impl.propagationStopped)
183         return false;
184     }
185     return true;
186   },
187
188   fireEventListeners_: function(node, event) {
189     var nodeImpl = privates(node).impl;
190     var listeners = nodeImpl.listeners[event.type];
191     if (!listeners)
192       return;
193     var eventPhase = event.eventPhase;
194     for (var i = 0; i < listeners.length; i++) {
195       if (eventPhase == Event.CAPTURING_PHASE && !listeners[i].capture)
196         continue;
197       if (eventPhase == Event.BUBBLING_PHASE && listeners[i].capture)
198         continue;
199
200       try {
201         listeners[i].callback(event);
202       } catch (e) {
203         console.error('Error in event handler for ' + event.type +
204                       'during phase ' + eventPhase + ': ' +
205                       e.message + '\nStack trace: ' + e.stack);
206       }
207     }
208   },
209
210   performAction_: function(actionType, opt_args) {
211     // Not yet initialized.
212     if (this.rootImpl.treeID === undefined ||
213         this.id === undefined) {
214       return;
215     }
216
217     // Check permissions.
218     if (!IsInteractPermitted()) {
219       throw new Error(actionType + ' requires {"desktop": true} or' +
220           ' {"interact": true} in the "automation" manifest key.');
221     }
222
223     automationInternal.performAction({ treeID: this.rootImpl.treeID,
224                                        automationNodeID: this.id,
225                                        actionType: actionType },
226                                      opt_args || {});
227   },
228
229   querySelectorCallback_: function(userCallback, resultAutomationNodeID) {
230     // resultAutomationNodeID could be zero or undefined or (unlikely) null;
231     // they all amount to the same thing here, which is that no node was
232     // returned.
233     if (!resultAutomationNodeID) {
234       userCallback(null);
235       return;
236     }
237     var resultNode = this.rootImpl.get(resultAutomationNodeID);
238     if (!resultNode) {
239       logging.WARNING('Query selector result not in tree: ' +
240                       resultAutomationNodeID);
241       userCallback(null);
242     }
243     userCallback(resultNode);
244   }
245 };
246
247 // Maps an attribute to its default value in an invalidated node.
248 // These attributes are taken directly from the Automation idl.
249 var AutomationAttributeDefaults = {
250   'id': -1,
251   'role': '',
252   'state': {},
253   'location': { left: 0, top: 0, width: 0, height: 0 }
254 };
255
256
257 var AutomationAttributeTypes = [
258   'boolAttributes',
259   'floatAttributes',
260   'htmlAttributes',
261   'intAttributes',
262   'intlistAttributes',
263   'stringAttributes'
264 ];
265
266
267 /**
268  * Maps an attribute name to another attribute who's value is an id or an array
269  * of ids referencing an AutomationNode.
270  * @param {!Object.<string, string>}
271  * @const
272  */
273 var ATTRIBUTE_NAME_TO_ATTRIBUTE_ID = {
274   'aria-activedescendant': 'activedescendantId',
275   'aria-controls': 'controlsIds',
276   'aria-describedby': 'describedbyIds',
277   'aria-flowto': 'flowtoIds',
278   'aria-labelledby': 'labelledbyIds',
279   'aria-owns': 'ownsIds'
280 };
281
282 /**
283  * A set of attributes ignored in the automation API.
284  * @param {!Object.<string, boolean>}
285  * @const
286  */
287 var ATTRIBUTE_BLACKLIST = {'activedescendantId': true,
288                            'childTreeId': true,
289                            'controlsIds': true,
290                            'describedbyIds': true,
291                            'flowtoIds': true,
292                            'labelledbyIds': true,
293                            'ownsIds': true
294 };
295
296
297 /**
298  * AutomationRootNode.
299  *
300  * An AutomationRootNode is the javascript end of an AXTree living in the
301  * browser. AutomationRootNode handles unserializing incremental updates from
302  * the source AXTree. Each update contains node data that form a complete tree
303  * after applying the update.
304  *
305  * A brief note about ids used through this class. The source AXTree assigns
306  * unique ids per node and we use these ids to build a hash to the actual
307  * AutomationNode object.
308  * Thus, tree traversals amount to a lookup in our hash.
309  *
310  * The tree itself is identified by the accessibility tree id of the
311  * renderer widget host.
312  * @constructor
313  */
314 function AutomationRootNodeImpl(treeID) {
315   AutomationNodeImpl.call(this, this);
316   this.treeID = treeID;
317   this.axNodeDataCache_ = {};
318 }
319
320 AutomationRootNodeImpl.prototype = {
321   __proto__: AutomationNodeImpl.prototype,
322
323   isRootNode: true,
324
325   get: function(id) {
326     if (id == undefined)
327       return undefined;
328
329     return this.axNodeDataCache_[id];
330   },
331
332   unserialize: function(update) {
333     var updateState = { pendingNodes: {}, newNodes: {} };
334     var oldRootId = this.id;
335
336     if (update.nodeIdToClear < 0) {
337         logging.WARNING('Bad nodeIdToClear: ' + update.nodeIdToClear);
338         lastError.set('automation',
339                       'Bad update received on automation tree',
340                       null,
341                       chrome);
342         return false;
343     } else if (update.nodeIdToClear > 0) {
344       var nodeToClear = this.axNodeDataCache_[update.nodeIdToClear];
345       if (!nodeToClear) {
346         logging.WARNING('Bad nodeIdToClear: ' + update.nodeIdToClear +
347                         ' (not in cache)');
348         lastError.set('automation',
349                       'Bad update received on automation tree',
350                       null,
351                       chrome);
352         return false;
353       }
354       if (nodeToClear === this.wrapper) {
355         this.invalidate_(nodeToClear);
356       } else {
357         var children = nodeToClear.children();
358         for (var i = 0; i < children.length; i++)
359           this.invalidate_(children[i]);
360         var nodeToClearImpl = privates(nodeToClear).impl;
361         nodeToClearImpl.childIds = []
362         updateState.pendingNodes[nodeToClearImpl.id] = nodeToClear;
363       }
364     }
365
366     for (var i = 0; i < update.nodes.length; i++) {
367       if (!this.updateNode_(update.nodes[i], updateState))
368         return false;
369     }
370
371     if (Object.keys(updateState.pendingNodes).length > 0) {
372       logging.WARNING('Nodes left pending by the update: ' +
373           $JSON.stringify(updateState.pendingNodes));
374       lastError.set('automation',
375                     'Bad update received on automation tree',
376                     null,
377                     chrome);
378       return false;
379     }
380     return true;
381   },
382
383   destroy: function() {
384     if (this.hostTree)
385       this.hostTree.childTree = undefined;
386     this.hostTree = undefined;
387
388     this.dispatchEvent(schema.EventType.destroyed);
389     this.invalidate_(this.wrapper);
390   },
391
392   onAccessibilityEvent: function(eventParams) {
393     if (!this.unserialize(eventParams.update)) {
394       logging.WARNING('unserialization failed');
395       return false;
396     }
397
398     var targetNode = this.get(eventParams.targetID);
399     if (targetNode) {
400       var targetNodeImpl = privates(targetNode).impl;
401       targetNodeImpl.dispatchEvent(eventParams.eventType);
402     } else {
403       logging.WARNING('Got ' + eventParams.eventType +
404                       ' event on unknown node: ' + eventParams.targetID +
405                       '; this: ' + this.id);
406     }
407     return true;
408   },
409
410   toString: function() {
411     function toStringInternal(node, indent) {
412       if (!node)
413         return '';
414       var output =
415           new Array(indent).join(' ') +
416           AutomationNodeImpl.prototype.toString.call(node) +
417           '\n';
418       indent += 2;
419       for (var i = 0; i < node.children().length; i++)
420         output += toStringInternal(node.children()[i], indent);
421       return output;
422     }
423     return toStringInternal(this, 0);
424   },
425
426   invalidate_: function(node) {
427     if (!node)
428       return;
429     var children = node.children();
430
431     for (var i = 0, child; child = children[i]; i++)
432       this.invalidate_(child);
433
434     // Retrieve the internal AutomationNodeImpl instance for this node.
435     // This object is not accessible outside of bindings code, but we can access
436     // it here.
437     var nodeImpl = privates(node).impl;
438     var id = nodeImpl.id;
439     for (var key in AutomationAttributeDefaults) {
440       nodeImpl[key] = AutomationAttributeDefaults[key];
441     }
442     nodeImpl.childIds = [];
443     nodeImpl.id = id;
444     delete this.axNodeDataCache_[id];
445   },
446
447   deleteOldChildren_: function(node, newChildIds) {
448     // Create a set of child ids in |src| for fast lookup, and return false
449     // if a duplicate is found;
450     var newChildIdSet = {};
451     for (var i = 0; i < newChildIds.length; i++) {
452       var childId = newChildIds[i];
453       if (newChildIdSet[childId]) {
454         logging.WARNING('Node ' + privates(node).impl.id +
455                         ' has duplicate child id ' + childId);
456         lastError.set('automation',
457                       'Bad update received on automation tree',
458                       null,
459                       chrome);
460         return false;
461       }
462       newChildIdSet[newChildIds[i]] = true;
463     }
464
465     // Delete the old children.
466     var nodeImpl = privates(node).impl;
467     var oldChildIds = nodeImpl.childIds;
468     for (var i = 0; i < oldChildIds.length;) {
469       var oldId = oldChildIds[i];
470       if (!newChildIdSet[oldId]) {
471         this.invalidate_(this.axNodeDataCache_[oldId]);
472         oldChildIds.splice(i, 1);
473       } else {
474         i++;
475       }
476     }
477     nodeImpl.childIds = oldChildIds;
478
479     return true;
480   },
481
482   createNewChildren_: function(node, newChildIds, updateState) {
483     logging.CHECK(node);
484     var success = true;
485
486     for (var i = 0; i < newChildIds.length; i++) {
487       var childId = newChildIds[i];
488       var childNode = this.axNodeDataCache_[childId];
489       if (childNode) {
490         if (childNode.parent() != node) {
491           var parentId = -1;
492           if (childNode.parent()) {
493             var parentImpl = privates(childNode.parent()).impl;
494             parentId = parentImpl.id;
495           }
496           // This is a serious error - nodes should never be reparented.
497           // If this case occurs, continue so this node isn't left in an
498           // inconsistent state, but return failure at the end.
499           logging.WARNING('Node ' + childId + ' reparented from ' +
500                           parentId + ' to ' + privates(node).impl.id);
501           lastError.set('automation',
502                         'Bad update received on automation tree',
503                         null,
504                         chrome);
505           success = false;
506           continue;
507         }
508       } else {
509         childNode = new AutomationNode(this);
510         this.axNodeDataCache_[childId] = childNode;
511         privates(childNode).impl.id = childId;
512         updateState.pendingNodes[childId] = childNode;
513         updateState.newNodes[childId] = childNode;
514       }
515       privates(childNode).impl.indexInParent = i;
516       privates(childNode).impl.parentID = privates(node).impl.id;
517     }
518
519     return success;
520   },
521
522   setData_: function(node, nodeData) {
523     var nodeImpl = privates(node).impl;
524
525     // TODO(dtseng): Make into set listing all hosting node roles.
526     if (nodeData.role == schema.RoleType.webView) {
527       if (nodeImpl.pendingChildFrame === undefined)
528         nodeImpl.pendingChildFrame = true;
529
530       if (nodeImpl.pendingChildFrame) {
531         nodeImpl.childTreeID = nodeData.intAttributes.childTreeId;
532         automationInternal.enableFrame(nodeImpl.childTreeID);
533         automationUtil.storeTreeCallback(nodeImpl.childTreeID, function(root) {
534           nodeImpl.pendingChildFrame = false;
535           nodeImpl.childTree = root;
536           privates(root).impl.hostTree = node;
537           nodeImpl.dispatchEvent(schema.EventType.childrenChanged);
538         });
539       }
540     }
541     for (var key in AutomationAttributeDefaults) {
542       if (key in nodeData)
543         nodeImpl[key] = nodeData[key];
544       else
545         nodeImpl[key] = AutomationAttributeDefaults[key];
546     }
547     for (var i = 0; i < AutomationAttributeTypes.length; i++) {
548       var attributeType = AutomationAttributeTypes[i];
549       for (var attributeName in nodeData[attributeType]) {
550         nodeImpl.attributesInternal[attributeName] =
551             nodeData[attributeType][attributeName];
552         if (ATTRIBUTE_BLACKLIST.hasOwnProperty(attributeName) ||
553             nodeImpl.attributes.hasOwnProperty(attributeName)) {
554           continue;
555         } else if (
556             ATTRIBUTE_NAME_TO_ATTRIBUTE_ID.hasOwnProperty(attributeName)) {
557           this.defineReadonlyAttribute_(nodeImpl,
558               attributeName,
559               true);
560         } else {
561           this.defineReadonlyAttribute_(nodeImpl,
562                                         attributeName);
563         }
564       }
565     }
566   },
567
568   defineReadonlyAttribute_: function(node, attributeName, opt_isIDRef) {
569     $Object.defineProperty(node.attributes, attributeName, {
570       enumerable: true,
571       get: function() {
572         if (opt_isIDRef) {
573           var attributeId = node.attributesInternal[
574               ATTRIBUTE_NAME_TO_ATTRIBUTE_ID[attributeName]];
575           if (Array.isArray(attributeId)) {
576             return attributeId.map(function(current) {
577               return node.rootImpl.get(current);
578             }, this);
579           }
580           return node.rootImpl.get(attributeId);
581         }
582         return node.attributesInternal[attributeName];
583       }.bind(this),
584     });
585   },
586
587   updateNode_: function(nodeData, updateState) {
588     var node = this.axNodeDataCache_[nodeData.id];
589     var didUpdateRoot = false;
590     if (node) {
591       delete updateState.pendingNodes[privates(node).impl.id];
592     } else {
593       if (nodeData.role != schema.RoleType.rootWebArea &&
594           nodeData.role != schema.RoleType.desktop) {
595         logging.WARNING(String(nodeData.id) +
596                      ' is not in the cache and not the new root.');
597         lastError.set('automation',
598                       'Bad update received on automation tree',
599                       null,
600                       chrome);
601         return false;
602       }
603       // |this| is an AutomationRootNodeImpl; retrieve the
604       // AutomationRootNode instance instead.
605       node = this.wrapper;
606       didUpdateRoot = true;
607       updateState.newNodes[this.id] = this.wrapper;
608     }
609     this.setData_(node, nodeData);
610
611     // TODO(aboxhall): send onChanged event?
612     logging.CHECK(node);
613     if (!this.deleteOldChildren_(node, nodeData.childIds)) {
614       if (didUpdateRoot) {
615         this.invalidate_(this.wrapper);
616       }
617       return false;
618     }
619     var nodeImpl = privates(node).impl;
620
621     var success = this.createNewChildren_(node,
622                                           nodeData.childIds,
623                                           updateState);
624     nodeImpl.childIds = nodeData.childIds;
625     this.axNodeDataCache_[nodeImpl.id] = node;
626
627     return success;
628   }
629 };
630
631
632 var AutomationNode = utils.expose('AutomationNode',
633                                   AutomationNodeImpl,
634                                   { functions: ['parent',
635                                                 'firstChild',
636                                                 'lastChild',
637                                                 'children',
638                                                 'previousSibling',
639                                                 'nextSibling',
640                                                 'doDefault',
641                                                 'focus',
642                                                 'makeVisible',
643                                                 'setSelection',
644                                                 'addEventListener',
645                                                 'removeEventListener',
646                                                 'querySelector',
647                                                 'toJSON'],
648                                     readonly: ['isRootNode',
649                                                'role',
650                                                'state',
651                                                'location',
652                                                'attributes',
653                                                'indexInParent',
654                                                'root'] });
655
656 var AutomationRootNode = utils.expose('AutomationRootNode',
657                                       AutomationRootNodeImpl,
658                                       { superclass: AutomationNode });
659
660 exports.AutomationNode = AutomationNode;
661 exports.AutomationRootNode = AutomationRootNode;