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 |
11 var lastError = require('lastError'); | 11 var lastError = require('lastError'); |
12 var logging = requireNative('logging'); | 12 var logging = requireNative('logging'); |
13 var schema = requireNative('automationInternal').GetSchemaAdditions(); | 13 var schema = requireNative('automationInternal').GetSchemaAdditions(); |
14 var utils = require('utils'); | 14 var utils = require('utils'); |
15 | 15 |
16 /** | 16 /** |
17 * Maps an accessibility tree id to an AutomationNode with role type webView. | |
18 * @type {!Object.<string, string>} | |
19 */ | |
20 var idToWebView_ = {}; | |
21 | |
22 /** | |
17 * A single node in the Automation tree. | 23 * A single node in the Automation tree. |
18 * @param {AutomationRootNodeImpl} root The root of the tree. | 24 * @param {AutomationRootNodeImpl} root The root of the tree. |
19 * @constructor | 25 * @constructor |
20 */ | 26 */ |
21 function AutomationNodeImpl(root) { | 27 function AutomationNodeImpl(root) { |
22 this.rootImpl = root; | 28 this.rootImpl = root; |
23 this.childIds = []; | 29 this.childIds = []; |
24 // Public attributes. No actual data gets set on this object. | 30 // Public attributes. No actual data gets set on this object. |
25 this.attributes = {}; | 31 this.attributes = {}; |
26 // Internal object holding all attributes. | 32 // Internal object holding all attributes. |
27 this.attributesInternal = {}; | 33 this.attributesInternal = {}; |
28 this.listeners = {}; | 34 this.listeners = {}; |
29 this.location = { left: 0, top: 0, width: 0, height: 0 }; | 35 this.location = { left: 0, top: 0, width: 0, height: 0 }; |
30 } | 36 } |
31 | 37 |
32 AutomationNodeImpl.prototype = { | 38 AutomationNodeImpl.prototype = { |
33 id: -1, | 39 id: -1, |
34 role: '', | 40 role: '', |
35 state: { busy: true }, | 41 state: { busy: true }, |
36 isRootNode: false, | 42 isRootNode: false, |
37 | 43 |
38 get root() { | 44 get root() { |
39 return this.rootImpl.wrapper; | 45 return this.rootImpl.wrapper; |
40 }, | 46 }, |
41 | 47 |
42 parent: function() { | 48 parent: function() { |
49 if (this.role == schema.RoleType.rootWebArea) { | |
50 if (idToWebView_[this.treeID]) | |
51 return idToWebView_[this.treeID]; | |
52 } | |
43 return this.rootImpl.get(this.parentID); | 53 return this.rootImpl.get(this.parentID); |
44 }, | 54 }, |
45 | 55 |
46 firstChild: function() { | 56 firstChild: function() { |
47 var node = this.rootImpl.get(this.childIds[0]); | 57 return this.lookupWebViewChild_() || this.rootImpl.get(this.childIds[0]); |
48 return node; | |
49 }, | 58 }, |
50 | 59 |
51 lastChild: function() { | 60 lastChild: function() { |
52 var childIds = this.childIds; | 61 var childIds = this.childIds; |
53 var node = this.rootImpl.get(childIds[childIds.length - 1]); | 62 return this.lookupWebViewChild_() || |
54 return node; | 63 this.rootImpl.get(childIds[childIds.length - 1]); |
55 }, | 64 }, |
56 | 65 |
57 children: function() { | 66 children: function() { |
58 var children = []; | 67 var children = []; |
59 for (var i = 0, childID; childID = this.childIds[i]; i++) { | 68 for (var i = 0, childID; childID = this.childIds[i]; i++) { |
60 logging.CHECK(this.rootImpl.get(childID)); | 69 logging.CHECK(this.rootImpl.get(childID)); |
61 children.push(this.rootImpl.get(childID)); | 70 children.push(this.rootImpl.get(childID)); |
62 } | 71 } |
63 return children; | 72 return children; |
64 }, | 73 }, |
(...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
111 listeners.splice(i, 1); | 120 listeners.splice(i, 1); |
112 } | 121 } |
113 } | 122 } |
114 }, | 123 }, |
115 | 124 |
116 dispatchEvent: function(eventType) { | 125 dispatchEvent: function(eventType) { |
117 var path = []; | 126 var path = []; |
118 var parent = this.parent(); | 127 var parent = this.parent(); |
119 while (parent) { | 128 while (parent) { |
120 path.push(parent); | 129 path.push(parent); |
121 // TODO(aboxhall/dtseng): handle unloaded parent node | |
122 parent = parent.parent(); | 130 parent = parent.parent(); |
123 } | 131 } |
124 var event = new AutomationEvent(eventType, this.wrapper); | 132 var event = new AutomationEvent(eventType, this.wrapper); |
125 | 133 |
126 // Dispatch the event through the propagation path in three phases: | 134 // Dispatch the event through the propagation path in three phases: |
127 // - capturing: starting from the root and going down to the target's parent | 135 // - capturing: starting from the root and going down to the target's parent |
128 // - targeting: dispatching the event on the target itself | 136 // - targeting: dispatching the event on the target itself |
129 // - bubbling: starting from the target's parent, going back up to the root. | 137 // - 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 | 138 // At any stage, a listener may call stopPropagation() on the event, which |
131 // will immediately stop event propagation through this path. | 139 // will immediately stop event propagation through this path. |
132 if (this.dispatchEventAtCapturing_(event, path)) { | 140 if (this.dispatchEventAtCapturing_(event, path)) { |
133 if (this.dispatchEventAtTargeting_(event, path)) | 141 if (this.dispatchEventAtTargeting_(event, path)) |
134 this.dispatchEventAtBubbling_(event, path); | 142 this.dispatchEventAtBubbling_(event, path); |
135 } | 143 } |
136 }, | 144 }, |
137 | 145 |
138 toString: function() { | 146 toString: function() { |
139 return 'node id=' + this.id + | 147 return 'node id=' + this.id + |
140 ' role=' + this.role + | 148 ' role=' + this.role + |
141 ' state=' + $JSON.stringify(this.state) + | 149 ' state=' + $JSON.stringify(this.state) + |
142 ' parentID=' + this.parentID + | 150 ' parentID=' + this.parentID + |
143 ' childIds=' + $JSON.stringify(this.childIds) + | 151 ' childIds=' + $JSON.stringify(this.childIds) + |
144 ' attributes=' + $JSON.stringify(this.attributes); | 152 ' attributes=' + $JSON.stringify(this.attributes); |
145 }, | 153 }, |
146 | 154 |
155 lookupWebViewChild_: function() { | |
156 if (this.role != schema.RoleType.webView) | |
157 return null; | |
158 | |
159 return automationUtil.idToAutomationRootNode[this.childTreeID]; | |
160 }, | |
161 | |
147 dispatchEventAtCapturing_: function(event, path) { | 162 dispatchEventAtCapturing_: function(event, path) { |
148 privates(event).impl.eventPhase = Event.CAPTURING_PHASE; | 163 privates(event).impl.eventPhase = Event.CAPTURING_PHASE; |
149 for (var i = path.length - 1; i >= 0; i--) { | 164 for (var i = path.length - 1; i >= 0; i--) { |
150 this.fireEventListeners_(path[i], event); | 165 this.fireEventListeners_(path[i], event); |
151 if (privates(event).impl.propagationStopped) | 166 if (privates(event).impl.propagationStopped) |
152 return false; | 167 return false; |
153 } | 168 } |
154 return true; | 169 return true; |
155 }, | 170 }, |
156 | 171 |
(...skipping 30 matching lines...) Expand all Loading... | |
187 } catch (e) { | 202 } catch (e) { |
188 console.error('Error in event handler for ' + event.type + | 203 console.error('Error in event handler for ' + event.type + |
189 'during phase ' + eventPhase + ': ' + | 204 'during phase ' + eventPhase + ': ' + |
190 e.message + '\nStack trace: ' + e.stack); | 205 e.message + '\nStack trace: ' + e.stack); |
191 } | 206 } |
192 } | 207 } |
193 }, | 208 }, |
194 | 209 |
195 performAction_: function(actionType, opt_args) { | 210 performAction_: function(actionType, opt_args) { |
196 // Not yet initialized. | 211 // Not yet initialized. |
197 if (this.rootImpl.processID === undefined || | 212 if (this.rootImpl.treeID === undefined || |
198 this.rootImpl.routingID === undefined || | |
199 this.id === undefined) { | 213 this.id === undefined) { |
200 return; | 214 return; |
201 } | 215 } |
202 | 216 |
203 // Check permissions. | 217 // Check permissions. |
204 if (!IsInteractPermitted()) { | 218 if (!IsInteractPermitted()) { |
205 throw new Error(actionType + ' requires {"desktop": true} or' + | 219 throw new Error(actionType + ' requires {"desktop": true} or' + |
206 ' {"interact": true} in the "automation" manifest key.'); | 220 ' {"interact": true} in the "automation" manifest key.'); |
207 } | 221 } |
208 | 222 |
209 automationInternal.performAction({ processID: this.rootImpl.processID, | 223 automationInternal.performAction({ treeID: this.rootImpl.treeID, |
210 routingID: this.rootImpl.routingID, | |
211 automationNodeID: this.id, | 224 automationNodeID: this.id, |
212 actionType: actionType }, | 225 actionType: actionType }, |
213 opt_args || {}); | 226 opt_args || {}); |
214 } | 227 } |
215 }; | 228 }; |
216 | 229 |
217 // Maps an attribute to its default value in an invalidated node. | 230 // Maps an attribute to its default value in an invalidated node. |
218 // These attributes are taken directly from the Automation idl. | 231 // These attributes are taken directly from the Automation idl. |
219 var AutomationAttributeDefaults = { | 232 var AutomationAttributeDefaults = { |
220 'id': -1, | 233 'id': -1, |
(...skipping 27 matching lines...) Expand all Loading... | |
248 'aria-labelledby': 'labelledbyIds', | 261 'aria-labelledby': 'labelledbyIds', |
249 'aria-owns': 'ownsIds' | 262 'aria-owns': 'ownsIds' |
250 }; | 263 }; |
251 | 264 |
252 /** | 265 /** |
253 * A set of attributes ignored in the automation API. | 266 * A set of attributes ignored in the automation API. |
254 * @param {!Object.<string, boolean>} | 267 * @param {!Object.<string, boolean>} |
255 * @const | 268 * @const |
256 */ | 269 */ |
257 var ATTRIBUTE_BLACKLIST = {'activedescendantId': true, | 270 var ATTRIBUTE_BLACKLIST = {'activedescendantId': true, |
271 'childTreeId': true, | |
258 'controlsIds': true, | 272 'controlsIds': true, |
259 'describedbyIds': true, | 273 'describedbyIds': true, |
260 'flowtoIds': true, | 274 'flowtoIds': true, |
261 'labelledbyIds': true, | 275 'labelledbyIds': true, |
262 'ownsIds': true | 276 'ownsIds': true |
263 }; | 277 }; |
264 | 278 |
265 | 279 |
266 /** | 280 /** |
267 * AutomationRootNode. | 281 * AutomationRootNode. |
268 * | 282 * |
269 * An AutomationRootNode is the javascript end of an AXTree living in the | 283 * An AutomationRootNode is the javascript end of an AXTree living in the |
270 * browser. AutomationRootNode handles unserializing incremental updates from | 284 * browser. AutomationRootNode handles unserializing incremental updates from |
271 * the source AXTree. Each update contains node data that form a complete tree | 285 * the source AXTree. Each update contains node data that form a complete tree |
272 * after applying the update. | 286 * after applying the update. |
273 * | 287 * |
274 * A brief note about ids used through this class. The source AXTree assigns | 288 * 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 | 289 * unique ids per node and we use these ids to build a hash to the actual |
276 * AutomationNode object. | 290 * AutomationNode object. |
277 * Thus, tree traversals amount to a lookup in our hash. | 291 * Thus, tree traversals amount to a lookup in our hash. |
278 * | 292 * |
279 * The tree itself is identified by the process id and routing id of the | 293 * The tree itself is identified by the accessibility tree id of the |
280 * renderer widget host. | 294 * renderer widget host. |
281 * @constructor | 295 * @constructor |
282 */ | 296 */ |
283 function AutomationRootNodeImpl(processID, routingID) { | 297 function AutomationRootNodeImpl(treeID) { |
284 AutomationNodeImpl.call(this, this); | 298 AutomationNodeImpl.call(this, this); |
285 this.processID = processID; | 299 this.treeID = treeID; |
286 this.routingID = routingID; | |
287 this.axNodeDataCache_ = {}; | 300 this.axNodeDataCache_ = {}; |
288 } | 301 } |
289 | 302 |
290 AutomationRootNodeImpl.prototype = { | 303 AutomationRootNodeImpl.prototype = { |
291 __proto__: AutomationNodeImpl.prototype, | 304 __proto__: AutomationNodeImpl.prototype, |
292 | 305 |
293 isRootNode: true, | 306 isRootNode: true, |
294 | 307 |
308 role: 'rootWebArea', | |
aboxhall
2014/11/03 17:15:49
Why is this necessary? Won't this come down in the
David Tseng
2014/11/03 19:31:53
It was (if we give callers the placeholder node) w
| |
309 | |
295 get: function(id) { | 310 get: function(id) { |
296 if (id == undefined) | 311 if (id == undefined) |
297 return undefined; | 312 return undefined; |
298 | 313 |
299 return this.axNodeDataCache_[id]; | 314 return this.axNodeDataCache_[id]; |
300 }, | 315 }, |
301 | 316 |
302 unserialize: function(update) { | 317 unserialize: function(update) { |
303 var updateState = { pendingNodes: {}, newNodes: {} }; | 318 var updateState = { pendingNodes: {}, newNodes: {} }; |
304 var oldRootId = this.id; | 319 var oldRootId = this.id; |
(...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
346 null, | 361 null, |
347 chrome); | 362 chrome); |
348 return false; | 363 return false; |
349 } | 364 } |
350 return true; | 365 return true; |
351 }, | 366 }, |
352 | 367 |
353 destroy: function() { | 368 destroy: function() { |
354 this.dispatchEvent(schema.EventType.destroyed); | 369 this.dispatchEvent(schema.EventType.destroyed); |
355 this.invalidate_(this.wrapper); | 370 this.invalidate_(this.wrapper); |
371 idToWebView_[this.treeID] = undefined; | |
aboxhall
2014/11/03 17:15:49
delete idToWebView_[this.treeID] does the same thi
David Tseng
2014/11/03 19:31:52
Obsolete.
| |
356 }, | 372 }, |
357 | 373 |
358 onAccessibilityEvent: function(eventParams) { | 374 onAccessibilityEvent: function(eventParams) { |
359 if (!this.unserialize(eventParams.update)) { | 375 if (!this.unserialize(eventParams.update)) { |
360 logging.WARNING('unserialization failed'); | 376 logging.WARNING('unserialization failed'); |
361 return false; | 377 return false; |
362 } | 378 } |
363 | 379 |
364 var targetNode = this.get(eventParams.targetID); | 380 var targetNode = this.get(eventParams.targetID); |
365 if (targetNode) { | 381 if (targetNode) { |
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
399 | 415 |
400 // Retrieve the internal AutomationNodeImpl instance for this node. | 416 // Retrieve the internal AutomationNodeImpl instance for this node. |
401 // This object is not accessible outside of bindings code, but we can access | 417 // This object is not accessible outside of bindings code, but we can access |
402 // it here. | 418 // it here. |
403 var nodeImpl = privates(node).impl; | 419 var nodeImpl = privates(node).impl; |
404 var id = nodeImpl.id; | 420 var id = nodeImpl.id; |
405 for (var key in AutomationAttributeDefaults) { | 421 for (var key in AutomationAttributeDefaults) { |
406 nodeImpl[key] = AutomationAttributeDefaults[key]; | 422 nodeImpl[key] = AutomationAttributeDefaults[key]; |
407 } | 423 } |
408 nodeImpl.childIds = []; | 424 nodeImpl.childIds = []; |
409 nodeImpl.loaded = false; | |
410 nodeImpl.id = id; | 425 nodeImpl.id = id; |
411 delete this.axNodeDataCache_[id]; | 426 delete this.axNodeDataCache_[id]; |
412 }, | 427 }, |
413 | 428 |
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) { | 429 deleteOldChildren_: function(node, newChildIds) { |
423 // Create a set of child ids in |src| for fast lookup, and return false | 430 // Create a set of child ids in |src| for fast lookup, and return false |
424 // if a duplicate is found; | 431 // if a duplicate is found; |
425 var newChildIdSet = {}; | 432 var newChildIdSet = {}; |
426 for (var i = 0; i < newChildIds.length; i++) { | 433 for (var i = 0; i < newChildIds.length; i++) { |
427 var childId = newChildIds[i]; | 434 var childId = newChildIds[i]; |
428 if (newChildIdSet[childId]) { | 435 if (newChildIdSet[childId]) { |
429 logging.WARNING('Node ' + privates(node).impl.id + | 436 logging.WARNING('Node ' + privates(node).impl.id + |
430 ' has duplicate child id ' + childId); | 437 ' has duplicate child id ' + childId); |
431 lastError.set('automation', | 438 lastError.set('automation', |
(...skipping 18 matching lines...) Expand all Loading... | |
450 } | 457 } |
451 } | 458 } |
452 nodeImpl.childIds = oldChildIds; | 459 nodeImpl.childIds = oldChildIds; |
453 | 460 |
454 return true; | 461 return true; |
455 }, | 462 }, |
456 | 463 |
457 createNewChildren_: function(node, newChildIds, updateState) { | 464 createNewChildren_: function(node, newChildIds, updateState) { |
458 logging.CHECK(node); | 465 logging.CHECK(node); |
459 var success = true; | 466 var success = true; |
467 | |
460 for (var i = 0; i < newChildIds.length; i++) { | 468 for (var i = 0; i < newChildIds.length; i++) { |
461 var childId = newChildIds[i]; | 469 var childId = newChildIds[i]; |
462 var childNode = this.axNodeDataCache_[childId]; | 470 var childNode = this.axNodeDataCache_[childId]; |
463 if (childNode) { | 471 if (childNode) { |
464 if (childNode.parent() != node) { | 472 if (childNode.parent() != node) { |
465 var parentId = -1; | 473 var parentId = -1; |
466 if (childNode.parent()) { | 474 if (childNode.parent()) { |
467 var parentImpl = privates(childNode.parent()).impl; | 475 var parentImpl = privates(childNode.parent()).impl; |
468 parentId = parentImpl.id; | 476 parentId = parentImpl.id; |
469 } | 477 } |
(...skipping 18 matching lines...) Expand all Loading... | |
488 } | 496 } |
489 privates(childNode).impl.indexInParent = i; | 497 privates(childNode).impl.indexInParent = i; |
490 privates(childNode).impl.parentID = privates(node).impl.id; | 498 privates(childNode).impl.parentID = privates(node).impl.id; |
491 } | 499 } |
492 | 500 |
493 return success; | 501 return success; |
494 }, | 502 }, |
495 | 503 |
496 setData_: function(node, nodeData) { | 504 setData_: function(node, nodeData) { |
497 var nodeImpl = privates(node).impl; | 505 var nodeImpl = privates(node).impl; |
506 | |
507 if (nodeData.role == schema.RoleType.webView) { | |
aboxhall
2014/11/03 17:15:49
I think we should have a set of roles which can be
David Tseng
2014/11/03 19:31:53
Added TODO since that seems a bit early at this po
| |
508 if (nodeImpl.pendingChildFrame === undefined) | |
509 nodeImpl.pendingChildFrame = true; | |
aboxhall
2014/11/03 17:15:49
Do we want to expose the pendingChildFrame propert
David Tseng
2014/11/03 19:31:52
I'd prefer for the caller to check if there are ch
| |
510 | |
511 if (nodeImpl.pendingChildFrame) { | |
512 nodeImpl.childTreeID = nodeData.intAttributes.childTreeId; | |
513 idToWebView_[nodeImpl.childTreeID] = node; | |
aboxhall
2014/11/03 17:15:49
So the id in idToWebView is the ID of the frame it
David Tseng
2014/11/03 19:31:52
Obsolete with other change.
| |
514 automationInternal.enableFrame(nodeImpl.childTreeID); | |
515 automationUtil.storeTreeCallback(nodeImpl.childTreeID, function(root) { | |
aboxhall
2014/11/03 17:15:49
What's preventing us from setting root.id as the s
David Tseng
2014/11/03 19:31:52
I'm pretty sure node ids are not globally unique a
aboxhall
2014/11/03 19:40:18
Ah of course, they won't be unique if they're in d
| |
516 nodeImpl.pendingChildFrame = false; | |
517 nodeImpl.dispatchEvent(schema.EventType.childrenChanged); | |
518 }); | |
519 } | |
520 } | |
498 for (var key in AutomationAttributeDefaults) { | 521 for (var key in AutomationAttributeDefaults) { |
499 if (key in nodeData) | 522 if (key in nodeData) |
500 nodeImpl[key] = nodeData[key]; | 523 nodeImpl[key] = nodeData[key]; |
501 else | 524 else |
502 nodeImpl[key] = AutomationAttributeDefaults[key]; | 525 nodeImpl[key] = AutomationAttributeDefaults[key]; |
503 } | 526 } |
504 for (var i = 0; i < AutomationAttributeTypes.length; i++) { | 527 for (var i = 0; i < AutomationAttributeTypes.length; i++) { |
505 var attributeType = AutomationAttributeTypes[i]; | 528 var attributeType = AutomationAttributeTypes[i]; |
506 for (var attributeName in nodeData[attributeType]) { | 529 for (var attributeName in nodeData[attributeType]) { |
507 nodeImpl.attributesInternal[attributeName] = | 530 nodeImpl.attributesInternal[attributeName] = |
(...skipping 92 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
600 'setSelection', | 623 'setSelection', |
601 'addEventListener', | 624 'addEventListener', |
602 'removeEventListener'], | 625 'removeEventListener'], |
603 readonly: ['isRootNode', | 626 readonly: ['isRootNode', |
604 'role', | 627 'role', |
605 'state', | 628 'state', |
606 'location', | 629 'location', |
607 'attributes', | 630 'attributes', |
608 'root'] }); | 631 'root'] }); |
609 | 632 |
610 var AutomationRootNode = utils.expose('AutomationRootNode', | 633 var AutomationRootNode = utils.expose('AutomationRootNode', |
aboxhall
2014/11/03 17:15:49
Do we still need to expose this type at all?
David Tseng
2014/11/03 19:31:52
Still seems useful at least for testing.
| |
611 AutomationRootNodeImpl, | 634 AutomationRootNodeImpl, |
612 { superclass: AutomationNode, | 635 { superclass: AutomationNode }); |
613 functions: ['load'], | |
614 readonly: ['loaded'] }); | |
615 | 636 |
616 exports.AutomationNode = AutomationNode; | 637 exports.AutomationNode = AutomationNode; |
617 exports.AutomationRootNode = AutomationRootNode; | 638 exports.AutomationRootNode = AutomationRootNode; |
OLD | NEW |