OLD | NEW |
1 // Copyright 2014 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 var AutomationEvent = require('automationEvent').AutomationEvent; | 5 var AutomationEvent = require('automationEvent').AutomationEvent; |
6 var automationInternal = | 6 var automationInternal = |
7 require('binding').Binding.create('automationInternal').generate(); | 7 require('binding').Binding.create('automationInternal').generate(); |
8 var IsInteractPermitted = | 8 var IsInteractPermitted = |
9 requireNative('automationInternal').IsInteractPermitted; | 9 requireNative('automationInternal').IsInteractPermitted; |
10 | 10 |
(...skipping 22 matching lines...) Expand all Loading... |
33 id: -1, | 33 id: -1, |
34 role: '', | 34 role: '', |
35 state: { busy: true }, | 35 state: { busy: true }, |
36 isRootNode: false, | 36 isRootNode: false, |
37 | 37 |
38 get root() { | 38 get root() { |
39 return this.rootImpl.wrapper; | 39 return this.rootImpl.wrapper; |
40 }, | 40 }, |
41 | 41 |
42 parent: function() { | 42 parent: function() { |
43 return this.rootImpl.get(this.parentID); | 43 return this.hostTree || this.rootImpl.get(this.parentID); |
44 }, | 44 }, |
45 | 45 |
46 firstChild: function() { | 46 firstChild: function() { |
47 var node = this.rootImpl.get(this.childIds[0]); | 47 return this.childTree || this.rootImpl.get(this.childIds[0]); |
48 return node; | |
49 }, | 48 }, |
50 | 49 |
51 lastChild: function() { | 50 lastChild: function() { |
52 var childIds = this.childIds; | 51 var childIds = this.childIds; |
53 var node = this.rootImpl.get(childIds[childIds.length - 1]); | 52 return this.childTree || this.rootImpl.get(childIds[childIds.length - 1]); |
54 return node; | |
55 }, | 53 }, |
56 | 54 |
57 children: function() { | 55 children: function() { |
58 var children = []; | 56 var children = []; |
59 for (var i = 0, childID; childID = this.childIds[i]; i++) { | 57 for (var i = 0, childID; childID = this.childIds[i]; i++) { |
60 logging.CHECK(this.rootImpl.get(childID)); | 58 logging.CHECK(this.rootImpl.get(childID)); |
61 children.push(this.rootImpl.get(childID)); | 59 children.push(this.rootImpl.get(childID)); |
62 } | 60 } |
63 return children; | 61 return children; |
64 }, | 62 }, |
(...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
106 removeEventListener: function(eventType, callback) { | 104 removeEventListener: function(eventType, callback) { |
107 if (this.listeners[eventType]) { | 105 if (this.listeners[eventType]) { |
108 var listeners = this.listeners[eventType]; | 106 var listeners = this.listeners[eventType]; |
109 for (var i = 0; i < listeners.length; i++) { | 107 for (var i = 0; i < listeners.length; i++) { |
110 if (callback === listeners[i].callback) | 108 if (callback === listeners[i].callback) |
111 listeners.splice(i, 1); | 109 listeners.splice(i, 1); |
112 } | 110 } |
113 } | 111 } |
114 }, | 112 }, |
115 | 113 |
| 114 toJSON: function() { |
| 115 return { treeID: this.treeID, |
| 116 id: this.id, |
| 117 role: this.role, |
| 118 attributes: this.attributes }; |
| 119 }, |
| 120 |
116 dispatchEvent: function(eventType) { | 121 dispatchEvent: function(eventType) { |
117 var path = []; | 122 var path = []; |
118 var parent = this.parent(); | 123 var parent = this.parent(); |
119 while (parent) { | 124 while (parent) { |
120 path.push(parent); | 125 path.push(parent); |
121 // TODO(aboxhall/dtseng): handle unloaded parent node | |
122 parent = parent.parent(); | 126 parent = parent.parent(); |
123 } | 127 } |
124 var event = new AutomationEvent(eventType, this.wrapper); | 128 var event = new AutomationEvent(eventType, this.wrapper); |
125 | 129 |
126 // Dispatch the event through the propagation path in three phases: | 130 // Dispatch the event through the propagation path in three phases: |
127 // - capturing: starting from the root and going down to the target's parent | 131 // - capturing: starting from the root and going down to the target's parent |
128 // - targeting: dispatching the event on the target itself | 132 // - targeting: dispatching the event on the target itself |
129 // - bubbling: starting from the target's parent, going back up to the root. | 133 // - bubbling: starting from the target's parent, going back up to the root. |
130 // At any stage, a listener may call stopPropagation() on the event, which | 134 // At any stage, a listener may call stopPropagation() on the event, which |
131 // will immediately stop event propagation through this path. | 135 // will immediately stop event propagation through this path. |
(...skipping 55 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
187 } catch (e) { | 191 } catch (e) { |
188 console.error('Error in event handler for ' + event.type + | 192 console.error('Error in event handler for ' + event.type + |
189 'during phase ' + eventPhase + ': ' + | 193 'during phase ' + eventPhase + ': ' + |
190 e.message + '\nStack trace: ' + e.stack); | 194 e.message + '\nStack trace: ' + e.stack); |
191 } | 195 } |
192 } | 196 } |
193 }, | 197 }, |
194 | 198 |
195 performAction_: function(actionType, opt_args) { | 199 performAction_: function(actionType, opt_args) { |
196 // Not yet initialized. | 200 // Not yet initialized. |
197 if (this.rootImpl.processID === undefined || | 201 if (this.rootImpl.treeID === undefined || |
198 this.rootImpl.routingID === undefined || | |
199 this.id === undefined) { | 202 this.id === undefined) { |
200 return; | 203 return; |
201 } | 204 } |
202 | 205 |
203 // Check permissions. | 206 // Check permissions. |
204 if (!IsInteractPermitted()) { | 207 if (!IsInteractPermitted()) { |
205 throw new Error(actionType + ' requires {"desktop": true} or' + | 208 throw new Error(actionType + ' requires {"desktop": true} or' + |
206 ' {"interact": true} in the "automation" manifest key.'); | 209 ' {"interact": true} in the "automation" manifest key.'); |
207 } | 210 } |
208 | 211 |
209 automationInternal.performAction({ processID: this.rootImpl.processID, | 212 automationInternal.performAction({ treeID: this.rootImpl.treeID, |
210 routingID: this.rootImpl.routingID, | |
211 automationNodeID: this.id, | 213 automationNodeID: this.id, |
212 actionType: actionType }, | 214 actionType: actionType }, |
213 opt_args || {}); | 215 opt_args || {}); |
214 } | 216 } |
215 }; | 217 }; |
216 | 218 |
217 // Maps an attribute to its default value in an invalidated node. | 219 // Maps an attribute to its default value in an invalidated node. |
218 // These attributes are taken directly from the Automation idl. | 220 // These attributes are taken directly from the Automation idl. |
219 var AutomationAttributeDefaults = { | 221 var AutomationAttributeDefaults = { |
220 'id': -1, | 222 'id': -1, |
(...skipping 27 matching lines...) Expand all Loading... |
248 'aria-labelledby': 'labelledbyIds', | 250 'aria-labelledby': 'labelledbyIds', |
249 'aria-owns': 'ownsIds' | 251 'aria-owns': 'ownsIds' |
250 }; | 252 }; |
251 | 253 |
252 /** | 254 /** |
253 * A set of attributes ignored in the automation API. | 255 * A set of attributes ignored in the automation API. |
254 * @param {!Object.<string, boolean>} | 256 * @param {!Object.<string, boolean>} |
255 * @const | 257 * @const |
256 */ | 258 */ |
257 var ATTRIBUTE_BLACKLIST = {'activedescendantId': true, | 259 var ATTRIBUTE_BLACKLIST = {'activedescendantId': true, |
| 260 'childTreeId': true, |
258 'controlsIds': true, | 261 'controlsIds': true, |
259 'describedbyIds': true, | 262 'describedbyIds': true, |
260 'flowtoIds': true, | 263 'flowtoIds': true, |
261 'labelledbyIds': true, | 264 'labelledbyIds': true, |
262 'ownsIds': true | 265 'ownsIds': true |
263 }; | 266 }; |
264 | 267 |
265 | 268 |
266 /** | 269 /** |
267 * AutomationRootNode. | 270 * AutomationRootNode. |
268 * | 271 * |
269 * An AutomationRootNode is the javascript end of an AXTree living in the | 272 * An AutomationRootNode is the javascript end of an AXTree living in the |
270 * browser. AutomationRootNode handles unserializing incremental updates from | 273 * browser. AutomationRootNode handles unserializing incremental updates from |
271 * the source AXTree. Each update contains node data that form a complete tree | 274 * the source AXTree. Each update contains node data that form a complete tree |
272 * after applying the update. | 275 * after applying the update. |
273 * | 276 * |
274 * A brief note about ids used through this class. The source AXTree assigns | 277 * A brief note about ids used through this class. The source AXTree assigns |
275 * unique ids per node and we use these ids to build a hash to the actual | 278 * unique ids per node and we use these ids to build a hash to the actual |
276 * AutomationNode object. | 279 * AutomationNode object. |
277 * Thus, tree traversals amount to a lookup in our hash. | 280 * Thus, tree traversals amount to a lookup in our hash. |
278 * | 281 * |
279 * The tree itself is identified by the process id and routing id of the | 282 * The tree itself is identified by the accessibility tree id of the |
280 * renderer widget host. | 283 * renderer widget host. |
281 * @constructor | 284 * @constructor |
282 */ | 285 */ |
283 function AutomationRootNodeImpl(processID, routingID) { | 286 function AutomationRootNodeImpl(treeID) { |
284 AutomationNodeImpl.call(this, this); | 287 AutomationNodeImpl.call(this, this); |
285 this.processID = processID; | 288 this.treeID = treeID; |
286 this.routingID = routingID; | |
287 this.axNodeDataCache_ = {}; | 289 this.axNodeDataCache_ = {}; |
288 } | 290 } |
289 | 291 |
290 AutomationRootNodeImpl.prototype = { | 292 AutomationRootNodeImpl.prototype = { |
291 __proto__: AutomationNodeImpl.prototype, | 293 __proto__: AutomationNodeImpl.prototype, |
292 | 294 |
293 isRootNode: true, | 295 isRootNode: true, |
294 | 296 |
295 get: function(id) { | 297 get: function(id) { |
296 if (id == undefined) | 298 if (id == undefined) |
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
344 lastError.set('automation', | 346 lastError.set('automation', |
345 'Bad update received on automation tree', | 347 'Bad update received on automation tree', |
346 null, | 348 null, |
347 chrome); | 349 chrome); |
348 return false; | 350 return false; |
349 } | 351 } |
350 return true; | 352 return true; |
351 }, | 353 }, |
352 | 354 |
353 destroy: function() { | 355 destroy: function() { |
| 356 if (this.hostTree) |
| 357 this.hostTree.childTree = undefined; |
| 358 this.hostTree = undefined; |
| 359 |
354 this.dispatchEvent(schema.EventType.destroyed); | 360 this.dispatchEvent(schema.EventType.destroyed); |
355 this.invalidate_(this.wrapper); | 361 this.invalidate_(this.wrapper); |
356 }, | 362 }, |
357 | 363 |
358 onAccessibilityEvent: function(eventParams) { | 364 onAccessibilityEvent: function(eventParams) { |
359 if (!this.unserialize(eventParams.update)) { | 365 if (!this.unserialize(eventParams.update)) { |
360 logging.WARNING('unserialization failed'); | 366 logging.WARNING('unserialization failed'); |
361 return false; | 367 return false; |
362 } | 368 } |
363 | 369 |
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
399 | 405 |
400 // Retrieve the internal AutomationNodeImpl instance for this node. | 406 // Retrieve the internal AutomationNodeImpl instance for this node. |
401 // This object is not accessible outside of bindings code, but we can access | 407 // This object is not accessible outside of bindings code, but we can access |
402 // it here. | 408 // it here. |
403 var nodeImpl = privates(node).impl; | 409 var nodeImpl = privates(node).impl; |
404 var id = nodeImpl.id; | 410 var id = nodeImpl.id; |
405 for (var key in AutomationAttributeDefaults) { | 411 for (var key in AutomationAttributeDefaults) { |
406 nodeImpl[key] = AutomationAttributeDefaults[key]; | 412 nodeImpl[key] = AutomationAttributeDefaults[key]; |
407 } | 413 } |
408 nodeImpl.childIds = []; | 414 nodeImpl.childIds = []; |
409 nodeImpl.loaded = false; | |
410 nodeImpl.id = id; | 415 nodeImpl.id = id; |
411 delete this.axNodeDataCache_[id]; | 416 delete this.axNodeDataCache_[id]; |
412 }, | 417 }, |
413 | 418 |
414 load: function(callback) { | |
415 // TODO(dtseng/aboxhall): Implement. | |
416 if (!this.loaded) | |
417 throw 'Unsupported state: root node is not loaded.'; | |
418 | |
419 setTimeout(callback, 0); | |
420 }, | |
421 | |
422 deleteOldChildren_: function(node, newChildIds) { | 419 deleteOldChildren_: function(node, newChildIds) { |
423 // Create a set of child ids in |src| for fast lookup, and return false | 420 // Create a set of child ids in |src| for fast lookup, and return false |
424 // if a duplicate is found; | 421 // if a duplicate is found; |
425 var newChildIdSet = {}; | 422 var newChildIdSet = {}; |
426 for (var i = 0; i < newChildIds.length; i++) { | 423 for (var i = 0; i < newChildIds.length; i++) { |
427 var childId = newChildIds[i]; | 424 var childId = newChildIds[i]; |
428 if (newChildIdSet[childId]) { | 425 if (newChildIdSet[childId]) { |
429 logging.WARNING('Node ' + privates(node).impl.id + | 426 logging.WARNING('Node ' + privates(node).impl.id + |
430 ' has duplicate child id ' + childId); | 427 ' has duplicate child id ' + childId); |
431 lastError.set('automation', | 428 lastError.set('automation', |
(...skipping 18 matching lines...) Expand all Loading... |
450 } | 447 } |
451 } | 448 } |
452 nodeImpl.childIds = oldChildIds; | 449 nodeImpl.childIds = oldChildIds; |
453 | 450 |
454 return true; | 451 return true; |
455 }, | 452 }, |
456 | 453 |
457 createNewChildren_: function(node, newChildIds, updateState) { | 454 createNewChildren_: function(node, newChildIds, updateState) { |
458 logging.CHECK(node); | 455 logging.CHECK(node); |
459 var success = true; | 456 var success = true; |
| 457 |
460 for (var i = 0; i < newChildIds.length; i++) { | 458 for (var i = 0; i < newChildIds.length; i++) { |
461 var childId = newChildIds[i]; | 459 var childId = newChildIds[i]; |
462 var childNode = this.axNodeDataCache_[childId]; | 460 var childNode = this.axNodeDataCache_[childId]; |
463 if (childNode) { | 461 if (childNode) { |
464 if (childNode.parent() != node) { | 462 if (childNode.parent() != node) { |
465 var parentId = -1; | 463 var parentId = -1; |
466 if (childNode.parent()) { | 464 if (childNode.parent()) { |
467 var parentImpl = privates(childNode.parent()).impl; | 465 var parentImpl = privates(childNode.parent()).impl; |
468 parentId = parentImpl.id; | 466 parentId = parentImpl.id; |
469 } | 467 } |
(...skipping 18 matching lines...) Expand all Loading... |
488 } | 486 } |
489 privates(childNode).impl.indexInParent = i; | 487 privates(childNode).impl.indexInParent = i; |
490 privates(childNode).impl.parentID = privates(node).impl.id; | 488 privates(childNode).impl.parentID = privates(node).impl.id; |
491 } | 489 } |
492 | 490 |
493 return success; | 491 return success; |
494 }, | 492 }, |
495 | 493 |
496 setData_: function(node, nodeData) { | 494 setData_: function(node, nodeData) { |
497 var nodeImpl = privates(node).impl; | 495 var nodeImpl = privates(node).impl; |
| 496 |
| 497 // TODO(dtseng): Make into set listing all hosting node roles. |
| 498 if (nodeData.role == schema.RoleType.webView) { |
| 499 if (nodeImpl.pendingChildFrame === undefined) |
| 500 nodeImpl.pendingChildFrame = true; |
| 501 |
| 502 if (nodeImpl.pendingChildFrame) { |
| 503 nodeImpl.childTreeID = nodeData.intAttributes.childTreeId; |
| 504 automationInternal.enableFrame(nodeImpl.childTreeID); |
| 505 automationUtil.storeTreeCallback(nodeImpl.childTreeID, function(root) { |
| 506 nodeImpl.pendingChildFrame = false; |
| 507 nodeImpl.childTree = root; |
| 508 privates(root).impl.hostTree = node; |
| 509 nodeImpl.dispatchEvent(schema.EventType.childrenChanged); |
| 510 }); |
| 511 } |
| 512 } |
498 for (var key in AutomationAttributeDefaults) { | 513 for (var key in AutomationAttributeDefaults) { |
499 if (key in nodeData) | 514 if (key in nodeData) |
500 nodeImpl[key] = nodeData[key]; | 515 nodeImpl[key] = nodeData[key]; |
501 else | 516 else |
502 nodeImpl[key] = AutomationAttributeDefaults[key]; | 517 nodeImpl[key] = AutomationAttributeDefaults[key]; |
503 } | 518 } |
504 for (var i = 0; i < AutomationAttributeTypes.length; i++) { | 519 for (var i = 0; i < AutomationAttributeTypes.length; i++) { |
505 var attributeType = AutomationAttributeTypes[i]; | 520 var attributeType = AutomationAttributeTypes[i]; |
506 for (var attributeName in nodeData[attributeType]) { | 521 for (var attributeName in nodeData[attributeType]) { |
507 nodeImpl.attributesInternal[attributeName] = | 522 nodeImpl.attributesInternal[attributeName] = |
(...skipping 84 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
592 'firstChild', | 607 'firstChild', |
593 'lastChild', | 608 'lastChild', |
594 'children', | 609 'children', |
595 'previousSibling', | 610 'previousSibling', |
596 'nextSibling', | 611 'nextSibling', |
597 'doDefault', | 612 'doDefault', |
598 'focus', | 613 'focus', |
599 'makeVisible', | 614 'makeVisible', |
600 'setSelection', | 615 'setSelection', |
601 'addEventListener', | 616 'addEventListener', |
602 'removeEventListener'], | 617 'removeEventListener', |
| 618 'toJSON'], |
603 readonly: ['isRootNode', | 619 readonly: ['isRootNode', |
604 'role', | 620 'role', |
605 'state', | 621 'state', |
606 'location', | 622 'location', |
607 'attributes', | 623 'attributes', |
| 624 'indexInParent', |
608 'root'] }); | 625 'root'] }); |
609 | 626 |
610 var AutomationRootNode = utils.expose('AutomationRootNode', | 627 var AutomationRootNode = utils.expose('AutomationRootNode', |
611 AutomationRootNodeImpl, | 628 AutomationRootNodeImpl, |
612 { superclass: AutomationNode, | 629 { superclass: AutomationNode }); |
613 functions: ['load'], | |
614 readonly: ['loaded'] }); | |
615 | 630 |
616 exports.AutomationNode = AutomationNode; | 631 exports.AutomationNode = AutomationNode; |
617 exports.AutomationRootNode = AutomationRootNode; | 632 exports.AutomationRootNode = AutomationRootNode; |
OLD | NEW |