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