• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
5var AutomationEvent = require('automationEvent').AutomationEvent;
6var automationInternal =
7    require('binding').Binding.create('automationInternal').generate();
8var utils = require('utils');
9var 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 */
17function 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
25AutomationNodeImpl.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.
209var AutomationAttributeDefaults = {
210  'id': -1,
211  'role': '',
212  'state': {},
213  'location': { left: 0, top: 0, width: 0, height: 0 }
214};
215
216
217var 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 */
244function AutomationRootNodeImpl(processID, routingID) {
245  AutomationNodeImpl.call(this, this);
246  this.processID = processID;
247  this.routingID = routingID;
248  this.axNodeDataCache_ = {};
249}
250
251AutomationRootNodeImpl.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
372var 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
393var AutomationRootNode = utils.expose('AutomationRootNode',
394                                      AutomationRootNodeImpl,
395                                      { superclass: AutomationNode,
396                                        functions: ['load'],
397                                        readonly: ['loaded'] });
398
399exports.AutomationNode = AutomationNode;
400exports.AutomationRootNode = AutomationRootNode;
401