Home | History | Annotate | Download | only in automation
      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 utils = require('utils');
      9 var IsInteractPermitted =
     10     requireNative('automationInternal').IsInteractPermitted;
     11 
     12 /**
     13  * A single node in the Automation tree.
     14  * @param {AutomationRootNodeImpl} root The root of the tree.
     15  * @constructor
     16  */
     17 function AutomationNodeImpl(root) {
     18   this.rootImpl = root;
     19   this.childIds = [];
     20   this.attributes = {};
     21   this.listeners = {};
     22   this.location = { left: 0, top: 0, width: 0, height: 0 };
     23 }
     24 
     25 AutomationNodeImpl.prototype = {
     26   id: -1,
     27   role: '',
     28   state: { busy: true },
     29   isRootNode: false,
     30 
     31   get root() {
     32     return this.rootImpl.wrapper;
     33   },
     34 
     35   parent: function() {
     36     return this.rootImpl.get(this.parentID);
     37   },
     38 
     39   firstChild: function() {
     40     var node = this.rootImpl.get(this.childIds[0]);
     41     return node;
     42   },
     43 
     44   lastChild: function() {
     45     var childIds = this.childIds;
     46     var node = this.rootImpl.get(childIds[childIds.length - 1]);
     47     return node;
     48   },
     49 
     50   children: function() {
     51     var children = [];
     52     for (var i = 0, childID; childID = this.childIds[i]; i++)
     53       children.push(this.rootImpl.get(childID));
     54     return children;
     55   },
     56 
     57   previousSibling: function() {
     58     var parent = this.parent();
     59     if (parent && this.indexInParent > 0)
     60       return parent.children()[this.indexInParent - 1];
     61     return undefined;
     62   },
     63 
     64   nextSibling: function() {
     65     var parent = this.parent();
     66     if (parent && this.indexInParent < parent.children().length)
     67       return parent.children()[this.indexInParent + 1];
     68     return undefined;
     69   },
     70 
     71   doDefault: function() {
     72     this.performAction_('doDefault');
     73   },
     74 
     75   focus: function(opt_callback) {
     76     this.performAction_('focus');
     77   },
     78 
     79   makeVisible: function(opt_callback) {
     80     this.performAction_('makeVisible');
     81   },
     82 
     83   setSelection: function(startIndex, endIndex, opt_callback) {
     84     this.performAction_('setSelection',
     85                         { startIndex: startIndex,
     86                           endIndex: endIndex });
     87   },
     88 
     89   addEventListener: function(eventType, callback, capture) {
     90     this.removeEventListener(eventType, callback);
     91     if (!this.listeners[eventType])
     92       this.listeners[eventType] = [];
     93     this.listeners[eventType].push({callback: callback, capture: !!capture});
     94   },
     95 
     96   // TODO(dtseng/aboxhall): Check this impl against spec.
     97   removeEventListener: function(eventType, callback) {
     98     if (this.listeners[eventType]) {
     99       var listeners = this.listeners[eventType];
    100       for (var i = 0; i < listeners.length; i++) {
    101         if (callback === listeners[i].callback)
    102           listeners.splice(i, 1);
    103       }
    104     }
    105   },
    106 
    107   dispatchEvent: function(eventType) {
    108     var path = [];
    109     var parent = this.parent();
    110     while (parent) {
    111       path.push(parent);
    112       // TODO(aboxhall/dtseng): handle unloaded parent node
    113       parent = parent.parent();
    114     }
    115     var event = new AutomationEvent(eventType, this.wrapper);
    116 
    117     // Dispatch the event through the propagation path in three phases:
    118     // - capturing: starting from the root and going down to the target's parent
    119     // - targeting: dispatching the event on the target itself
    120     // - bubbling: starting from the target's parent, going back up to the root.
    121     // At any stage, a listener may call stopPropagation() on the event, which
    122     // will immediately stop event propagation through this path.
    123     if (this.dispatchEventAtCapturing_(event, path)) {
    124       if (this.dispatchEventAtTargeting_(event, path))
    125         this.dispatchEventAtBubbling_(event, path);
    126     }
    127   },
    128 
    129   toString: function() {
    130     return 'node id=' + this.id +
    131         ' role=' + this.role +
    132         ' state=' + JSON.stringify(this.state) +
    133         ' childIds=' + JSON.stringify(this.childIds) +
    134         ' attributes=' + JSON.stringify(this.attributes);
    135   },
    136 
    137   dispatchEventAtCapturing_: function(event, path) {
    138     privates(event).impl.eventPhase = Event.CAPTURING_PHASE;
    139     for (var i = path.length - 1; i >= 0; i--) {
    140       this.fireEventListeners_(path[i], event);
    141       if (privates(event).impl.propagationStopped)
    142         return false;
    143     }
    144     return true;
    145   },
    146 
    147   dispatchEventAtTargeting_: function(event) {
    148     privates(event).impl.eventPhase = Event.AT_TARGET;
    149     this.fireEventListeners_(this.wrapper, event);
    150     return !privates(event).impl.propagationStopped;
    151   },
    152 
    153   dispatchEventAtBubbling_: function(event, path) {
    154     privates(event).impl.eventPhase = Event.BUBBLING_PHASE;
    155     for (var i = 0; i < path.length; i++) {
    156       this.fireEventListeners_(path[i], event);
    157       if (privates(event).impl.propagationStopped)
    158         return false;
    159     }
    160     return true;
    161   },
    162 
    163   fireEventListeners_: function(node, event) {
    164     var nodeImpl = privates(node).impl;
    165     var listeners = nodeImpl.listeners[event.type];
    166     if (!listeners)
    167       return;
    168     var eventPhase = event.eventPhase;
    169     for (var i = 0; i < listeners.length; i++) {
    170       if (eventPhase == Event.CAPTURING_PHASE && !listeners[i].capture)
    171         continue;
    172       if (eventPhase == Event.BUBBLING_PHASE && listeners[i].capture)
    173         continue;
    174 
    175       try {
    176         listeners[i].callback(event);
    177       } catch (e) {
    178         console.error('Error in event handler for ' + event.type +
    179                       'during phase ' + eventPhase + ': ' +
    180                       e.message + '\nStack trace: ' + e.stack);
    181       }
    182     }
    183   },
    184 
    185   performAction_: function(actionType, opt_args) {
    186     // Not yet initialized.
    187     if (this.rootImpl.processID === undefined ||
    188         this.rootImpl.routingID === undefined ||
    189         this.wrapper.id === undefined) {
    190       return;
    191     }
    192 
    193     // Check permissions.
    194     if (!IsInteractPermitted()) {
    195       throw new Error(actionType + ' requires {"desktop": true} or' +
    196           ' {"interact": true} in the "automation" manifest key.');
    197     }
    198 
    199     automationInternal.performAction({ processID: this.rootImpl.processID,
    200                                        routingID: this.rootImpl.routingID,
    201                                        automationNodeID: this.wrapper.id,
    202                                        actionType: actionType },
    203                                      opt_args || {});
    204   }
    205 };
    206 
    207 // Maps an attribute to its default value in an invalidated node.
    208 // These attributes are taken directly from the Automation idl.
    209 var AutomationAttributeDefaults = {
    210   'id': -1,
    211   'role': '',
    212   'state': {},
    213   'location': { left: 0, top: 0, width: 0, height: 0 }
    214 };
    215 
    216 
    217 var AutomationAttributeTypes = [
    218   'boolAttributes',
    219   'floatAttributes',
    220   'htmlAttributes',
    221   'intAttributes',
    222   'intlistAttributes',
    223   'stringAttributes'
    224 ];
    225 
    226 
    227 /**
    228  * AutomationRootNode.
    229  *
    230  * An AutomationRootNode is the javascript end of an AXTree living in the
    231  * browser. AutomationRootNode handles unserializing incremental updates from
    232  * the source AXTree. Each update contains node data that form a complete tree
    233  * after applying the update.
    234  *
    235  * A brief note about ids used through this class. The source AXTree assigns
    236  * unique ids per node and we use these ids to build a hash to the actual
    237  * AutomationNode object.
    238  * Thus, tree traversals amount to a lookup in our hash.
    239  *
    240  * The tree itself is identified by the process id and routing id of the
    241  * renderer widget host.
    242  * @constructor
    243  */
    244 function AutomationRootNodeImpl(processID, routingID) {
    245   AutomationNodeImpl.call(this, this);
    246   this.processID = processID;
    247   this.routingID = routingID;
    248   this.axNodeDataCache_ = {};
    249 }
    250 
    251 AutomationRootNodeImpl.prototype = {
    252   __proto__: AutomationNodeImpl.prototype,
    253 
    254   isRootNode: true,
    255 
    256   get: function(id) {
    257     return this.axNodeDataCache_[id];
    258   },
    259 
    260   invalidate: function(node) {
    261     if (!node)
    262       return;
    263 
    264     var children = node.children();
    265 
    266     for (var i = 0, child; child = children[i]; i++)
    267       this.invalidate(child);
    268 
    269     // Retrieve the internal AutomationNodeImpl instance for this node.
    270     // This object is not accessible outside of bindings code, but we can access
    271     // it here.
    272     var nodeImpl = privates(node).impl;
    273     var id = node.id;
    274     for (var key in AutomationAttributeDefaults) {
    275       nodeImpl[key] = AutomationAttributeDefaults[key];
    276     }
    277     nodeImpl.loaded = false;
    278     nodeImpl.id = id;
    279     this.axNodeDataCache_[id] = undefined;
    280   },
    281 
    282   update: function(data) {
    283     var didUpdateRoot = false;
    284 
    285     if (data.nodes.length == 0)
    286       return false;
    287 
    288     for (var i = 0; i < data.nodes.length; i++) {
    289       var nodeData = data.nodes[i];
    290       var node = this.axNodeDataCache_[nodeData.id];
    291       if (!node) {
    292         if (nodeData.role == 'rootWebArea' || nodeData.role == 'desktop') {
    293           // |this| is an AutomationRootNodeImpl; retrieve the
    294           // AutomationRootNode instance instead.
    295           node = this.wrapper;
    296           didUpdateRoot = true;
    297         } else {
    298           node = new AutomationNode(this);
    299         }
    300       }
    301       var nodeImpl = privates(node).impl;
    302 
    303       // Update children.
    304       var oldChildIDs = nodeImpl.childIds;
    305       var newChildIDs = nodeData.childIds || [];
    306       var newChildIDsHash = {};
    307 
    308       for (var j = 0, newId; newId = newChildIDs[j]; j++) {
    309         // Hash the new child ids for faster lookup.
    310         newChildIDsHash[newId] = newId;
    311 
    312         // We need to update all new children's parent id regardless.
    313         var childNode = this.get(newId);
    314         if (!childNode) {
    315           childNode = new AutomationNode(this);
    316           this.axNodeDataCache_[newId] = childNode;
    317           privates(childNode).impl.id = newId;
    318         }
    319         privates(childNode).impl.indexInParent = j;
    320         privates(childNode).impl.parentID = nodeData.id;
    321       }
    322 
    323       for (var k = 0, oldId; oldId = oldChildIDs[k]; k++) {
    324         // However, we must invalidate all old child ids that are no longer
    325         // children.
    326         if (!newChildIDsHash[oldId]) {
    327           this.invalidate(this.get(oldId));
    328         }
    329       }
    330 
    331       for (var key in AutomationAttributeDefaults) {
    332         if (key in nodeData)
    333           nodeImpl[key] = nodeData[key];
    334         else
    335           nodeImpl[key] = AutomationAttributeDefaults[key];
    336       }
    337       for (var attributeTypeIndex = 0;
    338            attributeTypeIndex < AutomationAttributeTypes.length;
    339            attributeTypeIndex++) {
    340         var attributeType = AutomationAttributeTypes[attributeTypeIndex];
    341         for (var attributeID in nodeData[attributeType]) {
    342           nodeImpl.attributes[attributeID] =
    343               nodeData[attributeType][attributeID];
    344         }
    345       }
    346       nodeImpl.childIds = newChildIDs;
    347       nodeImpl.loaded = true;
    348       this.axNodeDataCache_[node.id] = node;
    349     }
    350     var node = this.get(data.targetID);
    351     if (node)
    352       nodeImpl.dispatchEvent(data.eventType);
    353     return true;
    354   },
    355 
    356   toString: function() {
    357     function toStringInternal(node, indent) {
    358       if (!node)
    359         return '';
    360       var output =
    361         new Array(indent).join(' ') + privates(node).impl.toString() + '\n';
    362       indent += 2;
    363       for (var i = 0; i < node.children().length; i++)
    364         output += toStringInternal(node.children()[i], indent);
    365       return output;
    366     }
    367     return toStringInternal(this, 0);
    368   }
    369 };
    370 
    371 
    372 var AutomationNode = utils.expose('AutomationNode',
    373                                   AutomationNodeImpl,
    374                                   { functions: ['parent',
    375                                                 'firstChild',
    376                                                 'lastChild',
    377                                                 'children',
    378                                                 'previousSibling',
    379                                                 'nextSibling',
    380                                                 'doDefault',
    381                                                 'focus',
    382                                                 'makeVisible',
    383                                                 'setSelection',
    384                                                 'addEventListener',
    385                                                 'removeEventListener'],
    386                                     readonly: ['isRootNode',
    387                                                'id',
    388                                                'role',
    389                                                'state',
    390                                                'location',
    391                                                'attributes'] });
    392 
    393 var AutomationRootNode = utils.expose('AutomationRootNode',
    394                                       AutomationRootNodeImpl,
    395                                       { superclass: AutomationNode,
    396                                         functions: ['load'],
    397                                         readonly: ['loaded'] });
    398 
    399 exports.AutomationNode = AutomationNode;
    400 exports.AutomationRootNode = AutomationRootNode;
    401