Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(332)

Side by Side Diff: chrome/renderer/resources/extensions/automation/automation_node.js

Issue 667713006: Implement automatic load of composed/embedded automation trees (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@lkcr
Patch Set: Created 6 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
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
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
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
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
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
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
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
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
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;
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698