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

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

Issue 1155183006: Reimplement automation API on top of C++-backed AXTree. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@automation_faster_2
Patch Set: Rebase Created 5 years, 6 months 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 var GetRootID = requireNative('automationInternal').GetRootID;
11 var GetParentID = requireNative('automationInternal').GetParentID;
12 var GetChildCount = requireNative('automationInternal').GetChildCount;
13 var GetChildIDAtIndex = requireNative('automationInternal').GetChildIDAtIndex;
14 var GetIndexInParent = requireNative('automationInternal').GetIndexInParent;
15 var GetState = requireNative('automationInternal').GetState;
16 var GetRole = requireNative('automationInternal').GetRole;
17 var GetLocation = requireNative('automationInternal').GetLocation;
18 var GetStringAttribute = requireNative('automationInternal').GetStringAttribute;
19 var GetBoolAttribute = requireNative('automationInternal').GetBoolAttribute;
20 var GetIntAttribute = requireNative('automationInternal').GetIntAttribute;
21 var GetFloatAttribute = requireNative('automationInternal').GetFloatAttribute;
22 var GetIntListAttribute =
23 requireNative('automationInternal').GetIntListAttribute;
10 24
11 var lastError = require('lastError'); 25 var lastError = require('lastError');
12 var logging = requireNative('logging'); 26 var logging = requireNative('logging');
13 var schema = requireNative('automationInternal').GetSchemaAdditions(); 27 var schema = requireNative('automationInternal').GetSchemaAdditions();
14 var utils = require('utils'); 28 var utils = require('utils');
15 29
16 /** 30 /**
17 * A single node in the Automation tree. 31 * A single node in the Automation tree.
18 * @param {AutomationRootNodeImpl} root The root of the tree. 32 * @param {AutomationRootNodeImpl} root The root of the tree.
19 * @constructor 33 * @constructor
20 */ 34 */
21 function AutomationNodeImpl(root) { 35 function AutomationNodeImpl(root) {
22 this.rootImpl = root; 36 this.rootImpl = root;
23 this.childIds = []; 37 this.childIds = [];
24 // Public attributes. No actual data gets set on this object. 38 // Public attributes. No actual data gets set on this object.
25 this.attributes = {}; 39 this.attributes = {};
aboxhall 2015/06/11 17:42:46 Can this be removed too?
dmazzoni 2015/06/11 19:09:12 Yes! done.
26 // Internal object holding all attributes.
27 this.attributesInternal = {};
28 this.listeners = {}; 40 this.listeners = {};
29 this.location = { left: 0, top: 0, width: 0, height: 0 };
30 } 41 }
31 42
32 AutomationNodeImpl.prototype = { 43 AutomationNodeImpl.prototype = {
44 treeID: -1,
33 id: -1, 45 id: -1,
34 role: '', 46 role: '',
35 state: { busy: true }, 47 state: { busy: true },
36 isRootNode: false, 48 isRootNode: false,
37 49
38 get root() { 50 get root() {
39 return this.rootImpl.wrapper; 51 return this.rootImpl.wrapper;
40 }, 52 },
41 53
42 get parent() { 54 get parent() {
43 return this.hostTree || this.rootImpl.get(this.parentID); 55 if (this.hostNode)
56 return this.hostNode;
57 var parentID = GetParentID(this.treeID, this.id);
58 return this.rootImpl.get(parentID);
59 },
60
61 get state() {
62 return GetState(this.treeID, this.id);
63 },
64
65 get role() {
66 return GetRole(this.treeID, this.id);
67 },
68
69 get location() {
70 return GetLocation(this.treeID, this.id);
71 },
72
73 get indexInParent() {
74 return GetIndexInParent(this.treeID, this.id);
75 },
76
77 get childTree() {
78 var childTreeID = GetIntAttribute(this.treeID, this.id, 'childTreeId');
79 if (childTreeID)
80 return AutomationRootNodeImpl.get(childTreeID);
44 }, 81 },
45 82
46 get firstChild() { 83 get firstChild() {
47 return this.childTree || this.rootImpl.get(this.childIds[0]); 84 if (this.childTree)
85 return this.childTree;
86 if (!GetChildCount(this.treeID, this.id))
87 return undefined;
88 var firstChildID = GetChildIDAtIndex(this.treeID, this.id, 0);
89 return this.rootImpl.get(firstChildID);
48 }, 90 },
49 91
50 get lastChild() { 92 get lastChild() {
51 var childIds = this.childIds; 93 if (this.childTree)
52 return this.childTree || this.rootImpl.get(childIds[childIds.length - 1]); 94 return this.childTree;
95 var count = GetChildCount(this.treeID, this.id);
96 if (!count)
97 return undefined;
98 var lastChildID = GetChildIDAtIndex(this.treeID, this.id, count - 1);
99 return this.rootImpl.get(lastChildID);
53 }, 100 },
54 101
55 get children() { 102 get children() {
56 if (this.childTree) 103 if (this.childTree)
57 return [this.childTree]; 104 return [this.childTree];
58 105
59 var children = []; 106 var children = [];
60 for (var i = 0, childID; childID = this.childIds[i]; i++) { 107 var count = GetChildCount(this.treeID, this.id);
61 logging.CHECK(this.rootImpl.get(childID)); 108 for (var i = 0; i < count; ++i) {
62 children.push(this.rootImpl.get(childID)); 109 var childID = GetChildIDAtIndex(this.treeID, this.id, i);
110 var child = this.rootImpl.get(childID);
111 children.push(child);
63 } 112 }
64 return children; 113 return children;
65 }, 114 },
66 115
67 get previousSibling() { 116 get previousSibling() {
68 var parent = this.parent; 117 var parent = this.parent;
69 if (parent && this.indexInParent > 0) 118 var indexInParent = GetIndexInParent(this.treeID, this.id);
70 return parent.children[this.indexInParent - 1]; 119 if (parent && indexInParent > 0)
120 return parent.children[indexInParent - 1];
71 return undefined; 121 return undefined;
72 }, 122 },
73 123
74 get nextSibling() { 124 get nextSibling() {
75 var parent = this.parent; 125 var parent = this.parent;
76 if (parent && this.indexInParent < parent.children.length) 126 var indexInParent = GetIndexInParent(this.treeID, this.id);
77 return parent.children[this.indexInParent + 1]; 127 if (parent && indexInParent < parent.children.length)
128 return parent.children[indexInParent + 1];
78 return undefined; 129 return undefined;
79 }, 130 },
80 131
81 doDefault: function() { 132 doDefault: function() {
82 this.performAction_('doDefault'); 133 this.performAction_('doDefault');
83 }, 134 },
84 135
85 focus: function() { 136 focus: function() {
86 this.performAction_('focus'); 137 this.performAction_('focus');
87 }, 138 },
(...skipping 124 matching lines...) Expand 10 before | Expand all | Expand 10 after
212 var eventPhase = event.eventPhase; 263 var eventPhase = event.eventPhase;
213 for (var i = 0; i < listeners.length; i++) { 264 for (var i = 0; i < listeners.length; i++) {
214 if (eventPhase == Event.CAPTURING_PHASE && !listeners[i].capture) 265 if (eventPhase == Event.CAPTURING_PHASE && !listeners[i].capture)
215 continue; 266 continue;
216 if (eventPhase == Event.BUBBLING_PHASE && listeners[i].capture) 267 if (eventPhase == Event.BUBBLING_PHASE && listeners[i].capture)
217 continue; 268 continue;
218 269
219 try { 270 try {
220 listeners[i].callback(event); 271 listeners[i].callback(event);
221 } catch (e) { 272 } catch (e) {
222 console.error('Error in event handler for ' + event.type + 273 console.error('Error in event handler for ' + event.type +
aboxhall 2015/06/11 17:42:46 Maybe logging.WARN here as well (I know you didn't
dmazzoni 2015/06/11 19:09:12 Done.
223 'during phase ' + eventPhase + ': ' + 274 ' during phase ' + eventPhase + ': ' +
224 e.message + '\nStack trace: ' + e.stack); 275 e.message + '\nStack trace: ' + e.stack);
225 } 276 }
226 } 277 }
227 }, 278 },
228 279
229 performAction_: function(actionType, opt_args) { 280 performAction_: function(actionType, opt_args) {
230 // Not yet initialized. 281 // Not yet initialized.
231 if (this.rootImpl.treeID === undefined || 282 if (this.rootImpl.treeID === undefined ||
232 this.id === undefined) { 283 this.id === undefined) {
233 return; 284 return;
(...skipping 71 matching lines...) Expand 10 before | Expand all | Expand 10 after
305 return false; 356 return false;
306 357
307 if ('state' in params) { 358 if ('state' in params) {
308 for (var state in params.state) { 359 for (var state in params.state) {
309 if (params.state[state] != (state in this.state)) 360 if (params.state[state] != (state in this.state))
310 return false; 361 return false;
311 } 362 }
312 } 363 }
313 if ('attributes' in params) { 364 if ('attributes' in params) {
314 for (var attribute in params.attributes) { 365 for (var attribute in params.attributes) {
315 if (!(attribute in this.attributesInternal))
316 return false;
317
318 var attrValue = params.attributes[attribute]; 366 var attrValue = params.attributes[attribute];
319 if (typeof attrValue != 'object') { 367 if (typeof attrValue != 'object') {
320 if (this.attributesInternal[attribute] !== attrValue) 368 if (this[attribute] !== attrValue)
321 return false; 369 return false;
322 } else if (attrValue instanceof RegExp) { 370 } else if (attrValue instanceof RegExp) {
323 if (typeof this.attributesInternal[attribute] != 'string') 371 if (typeof this[attribute] != 'string')
324 return false; 372 return false;
325 if (!attrValue.test(this.attributesInternal[attribute])) 373 if (!attrValue.test(this[attribute]))
326 return false; 374 return false;
327 } else { 375 } else {
328 // TODO(aboxhall): handle intlist case. 376 // TODO(aboxhall): handle intlist case.
329 return false; 377 return false;
330 } 378 }
331 } 379 }
332 } 380 }
333 return true; 381 return true;
334 } 382 }
335 }; 383 };
336 384
337 // Maps an attribute to its default value in an invalidated node. 385 var publicAttributes = [];
338 // These attributes are taken directly from the Automation idl.
339 var AutomationAttributeDefaults = {
340 'id': -1,
341 'role': '',
342 'state': {},
343 'location': { left: 0, top: 0, width: 0, height: 0 }
344 };
345 386
346 387 function defineStringAttribute(attributeName) {
347 var AutomationAttributeTypes = [ 388 publicAttributes.push(attributeName);
348 'boolAttributes', 389 Object.defineProperty(AutomationNodeImpl.prototype, attributeName, {
349 'floatAttributes', 390 get: function() {
350 'htmlAttributes', 391 return GetStringAttribute(this.treeID, this.id, attributeName);
351 'intAttributes', 392 }
352 'intlistAttributes', 393 });
353 'stringAttributes'
354 ];
355
356 /**
357 * Maps an attribute name to another attribute who's value is an id or an array
358 * of ids referencing an AutomationNode.
359 * @param {!Object<string>}
360 * @const
361 */
362 var ATTRIBUTE_NAME_TO_ID_ATTRIBUTE = {
363 'aria-activedescendant': 'activedescendantId',
364 'aria-controls': 'controlsIds',
365 'aria-describedby': 'describedbyIds',
366 'aria-flowto': 'flowtoIds',
367 'aria-labelledby': 'labelledbyIds',
368 'aria-owns': 'ownsIds'
369 };
370
371 /**
372 * A set of attributes ignored in the automation API.
373 * @param {!Object<boolean>}
374 * @const
375 */
376 var ATTRIBUTE_BLACKLIST = {'activedescendantId': true,
377 'childTreeId': true,
378 'controlsIds': true,
379 'describedbyIds': true,
380 'flowtoIds': true,
381 'labelledbyIds': true,
382 'ownsIds': true
383 };
384
385 function defaultStringAttribute(opt_defaultVal) {
386 return { default: undefined, reflectFrom: 'stringAttributes' };
387 } 394 }
388 395
389 function defaultIntAttribute(opt_defaultVal) { 396 defineStringAttribute('accessKey');
aboxhall 2015/06/11 17:42:45 Maybe create arrays of each type of attribute and
aboxhall 2015/06/11 20:32:42 Ping on this.
dmazzoni 2015/06/12 17:55:37 Oops, this one's not included in the latest patch
dmazzoni 2015/06/12 18:08:00 Done now, is this better?
aboxhall 2015/06/12 20:43:10 This looks great, thanks!
390 var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : 0; 397 defineStringAttribute('action');
391 return { default: defaultVal, reflectFrom: 'intAttributes' }; 398 defineStringAttribute('ariaInvalidValue');
399 defineStringAttribute('autoComplete');
400 defineStringAttribute('containerLiveRelevant');
401 defineStringAttribute('containerLiveStatus');
402 defineStringAttribute('description');
403 defineStringAttribute('display');
404 defineStringAttribute('docDoctype');
405 defineStringAttribute('docMimetype');
406 defineStringAttribute('docTitle');
407 defineStringAttribute('docUrl');
408 defineStringAttribute('dropeffect');
409 defineStringAttribute('help');
410 defineStringAttribute('htmlTag');
411 defineStringAttribute('liveRelevant');
412 defineStringAttribute('liveStatus');
413 defineStringAttribute('name');
414 defineStringAttribute('placeholder');
415 defineStringAttribute('shortcut');
416 defineStringAttribute('textInputType');
417 defineStringAttribute('url');
418 defineStringAttribute('value');
419
420 function defineBoolAttribute(attributeName) {
421 publicAttributes.push(attributeName);
422 Object.defineProperty(AutomationNodeImpl.prototype, attributeName, {
423 get: function() {
424 return GetBoolAttribute(this.treeID, this.id, attributeName);
425 }
426 });
392 } 427 }
393 428
394 function defaultFloatAttribute(opt_defaultVal) { 429 defineBoolAttribute('ariaReadonly');
395 var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : 0; 430 defineBoolAttribute('buttonMixed');
396 return { default: defaultVal, reflectFrom: 'floatAttributes' }; 431 defineBoolAttribute('canSetValue');
432 defineBoolAttribute('canvasHasFallback');
433 defineBoolAttribute('containerLiveAtomic');
434 defineBoolAttribute('containerLiveBusy');
435 defineBoolAttribute('docLoaded');
436 defineBoolAttribute('grabbed');
437 defineBoolAttribute('isAxTreeHost');
438 defineBoolAttribute('liveAtomic');
439 defineBoolAttribute('liveBusy');
440 defineBoolAttribute('updateLocationOnly');
441
442 function defineIntAttribute(attributeName) {
443 publicAttributes.push(attributeName);
444 Object.defineProperty(AutomationNodeImpl.prototype, attributeName, {
445 get: function() {
446 return GetIntAttribute(this.treeID, this.id, attributeName);
447 }
448 });
397 } 449 }
398 450
399 function defaultBoolAttribute(opt_defaultVal) { 451 defineIntAttribute('backgroundColor');
400 var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : false; 452 defineIntAttribute('color');
401 return { default: defaultVal, reflectFrom: 'boolAttributes' }; 453 defineIntAttribute('colorValue');
454 defineIntAttribute('hierarchicalLevel');
455 defineIntAttribute('invalidState');
456 defineIntAttribute('posInSet');
457 defineIntAttribute('scrollX');
458 defineIntAttribute('scrollXMax');
459 defineIntAttribute('scrollXMin');
460 defineIntAttribute('scrollY');
461 defineIntAttribute('scrollYMax');
462 defineIntAttribute('scrollYMin');
463 defineIntAttribute('setSize');
464 defineIntAttribute('sortDirection');
465 defineIntAttribute('tableCellColumnIndex');
466 defineIntAttribute('tableCellColumnSpan');
467 defineIntAttribute('tableCellRowIndex');
468 defineIntAttribute('tableCellRowSpan');
469 defineIntAttribute('tableColumnCount');
470 defineIntAttribute('tableColumnIndex');
471 defineIntAttribute('tableRowCount');
472 defineIntAttribute('tableRowIndex');
473 defineIntAttribute('textDirection');
474 defineIntAttribute('textSelEnd');
475 defineIntAttribute('textSelStart');
476 defineIntAttribute('textStyle');
477
478 function defineNodeRefAttribute(srcAttributeName, dstAttributeName) {
479 publicAttributes.push(dstAttributeName);
480 Object.defineProperty(AutomationNodeImpl.prototype, dstAttributeName, {
481 get: function() {
482 var id = GetIntAttribute(this.treeID, this.id, srcAttributeName);
483 if (id)
484 return this.rootImpl.get(id);
485 else
486 return undefined;
487 }
488 });
402 } 489 }
403 490
404 function defaultHtmlAttribute(opt_defaultVal) { 491 defineNodeRefAttribute('activedescendantId', 'activedescendant');
405 var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : ''; 492 defineNodeRefAttribute('tableColumnHeaderId', 'tableColumnHeader');
406 return { default: defaultVal, reflectFrom: 'htmlAttributes' }; 493 defineNodeRefAttribute('tableHeaderId', 'tableHeader');
494 defineNodeRefAttribute('tableRowHeaderId', 'tableRowHeader');
495 defineNodeRefAttribute('titleUiElement', 'titleUIElement');
496
497 function defineIntListAttribute(attributeName) {
498 publicAttributes.push(attributeName);
499 Object.defineProperty(AutomationNodeImpl.prototype, attributeName, {
500 get: function() {
501 return GetIntListAttribute(this.treeID, this.id, attributeName);
502 }
503 });
407 } 504 }
408 505
409 function defaultIntListAttribute(opt_defaultVal) { 506 defineIntListAttribute('characterOffsets');
410 var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : []; 507 defineIntListAttribute('lineBreaks');
411 return { default: defaultVal, reflectFrom: 'intlistAttributes' }; 508 defineIntListAttribute('wordEnds');
509 defineIntListAttribute('wordStarts');
510
511 function defineNodeRefListAttribute(srcAttributeName, dstAttributeName) {
512 publicAttributes.push(dstAttributeName);
513 Object.defineProperty(AutomationNodeImpl.prototype, dstAttributeName, {
514 get: function() {
515 var ids = GetIntListAttribute(this.treeID, this.id, srcAttributeName);
516 if (!ids)
517 return undefined;
518 var result = [];
519 for (var i = 0; i < ids.length; ++i) {
520 var node = this.rootImpl.get(ids[i]);
521 if (node)
522 result.push(node);
523 }
524 return result;
525 }
526 });
412 } 527 }
413 528
414 function defaultNodeRefAttribute(idAttribute, opt_defaultVal) { 529 defineNodeRefListAttribute('cellIds', 'cells');
415 var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : null; 530 defineNodeRefListAttribute('controlsIds', 'controls');
416 return { default: defaultVal, 531 defineNodeRefListAttribute('describedbyIds', 'describedBy');
417 idFrom: 'intAttributes', 532 defineNodeRefListAttribute('flowtoIds', 'flowTo');
418 idAttribute: idAttribute, 533 defineNodeRefListAttribute('labelledbyIds', 'labelledBy');
419 isRef: true }; 534 defineNodeRefListAttribute('uniqueCellIds', 'uniqueCells');
535
536 function defineFloatAttribute(attributeName) {
537 publicAttributes.push(attributeName);
538 Object.defineProperty(AutomationNodeImpl.prototype, attributeName, {
539 get: function() {
540 return GetFloatAttribute(this.treeID, this.id, attributeName);
541 }
542 });
420 } 543 }
421 544
422 function defaultNodeRefListAttribute(idAttribute, opt_defaultVal) { 545 defineFloatAttribute('docLoadingProgress');
423 var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : []; 546 defineFloatAttribute('valueForRange');
424 return { default: [], 547 defineFloatAttribute('minValueForRange');
425 idFrom: 'intlistAttributes', 548 defineFloatAttribute('maxValueForRange');
426 idAttribute: idAttribute, 549 defineFloatAttribute('fontSize');
427 isRef: true };
428 }
429
430 // Maps an attribute to its default value in an invalidated node.
431 // These attributes are taken directly from the Automation idl.
432 var DefaultMixinAttributes = {
433 description: defaultStringAttribute(),
434 help: defaultStringAttribute(),
435 name: defaultStringAttribute(),
436 value: defaultStringAttribute(),
437 htmlTag: defaultStringAttribute(),
438 hierarchicalLevel: defaultIntAttribute(),
439 controls: defaultNodeRefListAttribute('controlsIds'),
440 describedby: defaultNodeRefListAttribute('describedbyIds'),
441 flowto: defaultNodeRefListAttribute('flowtoIds'),
442 labelledby: defaultNodeRefListAttribute('labelledbyIds'),
443 owns: defaultNodeRefListAttribute('ownsIds'),
444 wordStarts: defaultIntListAttribute(),
445 wordEnds: defaultIntListAttribute()
446 };
447
448 var ActiveDescendantMixinAttribute = {
449 activedescendant: defaultNodeRefAttribute('activedescendantId')
450 };
451
452 var LinkMixinAttributes = {
453 url: defaultStringAttribute()
454 };
455
456 var DocumentMixinAttributes = {
457 docUrl: defaultStringAttribute(),
458 docTitle: defaultStringAttribute(),
459 docLoaded: defaultStringAttribute(),
460 docLoadingProgress: defaultFloatAttribute()
461 };
462
463 var ScrollableMixinAttributes = {
464 scrollX: defaultIntAttribute(),
465 scrollXMin: defaultIntAttribute(),
466 scrollXMax: defaultIntAttribute(),
467 scrollY: defaultIntAttribute(),
468 scrollYMin: defaultIntAttribute(),
469 scrollYMax: defaultIntAttribute()
470 };
471
472 var EditableTextMixinAttributes = {
473 textSelStart: defaultIntAttribute(-1),
474 textSelEnd: defaultIntAttribute(-1),
475 textInputType: defaultStringAttribute()
476 };
477
478 var RangeMixinAttributes = {
479 valueForRange: defaultFloatAttribute(),
480 minValueForRange: defaultFloatAttribute(),
481 maxValueForRange: defaultFloatAttribute()
482 };
483
484 var TableMixinAttributes = {
485 tableRowCount: defaultIntAttribute(),
486 tableColumnCount: defaultIntAttribute()
487 };
488
489 var TableCellMixinAttributes = {
490 tableCellColumnIndex: defaultIntAttribute(),
491 tableCellColumnSpan: defaultIntAttribute(1),
492 tableCellRowIndex: defaultIntAttribute(),
493 tableCellRowSpan: defaultIntAttribute(1)
494 };
495
496 var LiveRegionMixinAttributes = {
497 containerLiveAtomic: defaultBoolAttribute(),
498 containerLiveBusy: defaultBoolAttribute(),
499 containerLiveRelevant: defaultStringAttribute(),
500 containerLiveStatus: defaultStringAttribute(),
501 };
502 550
503 /** 551 /**
504 * AutomationRootNode. 552 * AutomationRootNode.
505 * 553 *
506 * An AutomationRootNode is the javascript end of an AXTree living in the 554 * An AutomationRootNode is the javascript end of an AXTree living in the
507 * browser. AutomationRootNode handles unserializing incremental updates from 555 * browser. AutomationRootNode handles unserializing incremental updates from
508 * the source AXTree. Each update contains node data that form a complete tree 556 * the source AXTree. Each update contains node data that form a complete tree
509 * after applying the update. 557 * after applying the update.
510 * 558 *
511 * A brief note about ids used through this class. The source AXTree assigns 559 * A brief note about ids used through this class. The source AXTree assigns
512 * unique ids per node and we use these ids to build a hash to the actual 560 * unique ids per node and we use these ids to build a hash to the actual
513 * AutomationNode object. 561 * AutomationNode object.
514 * Thus, tree traversals amount to a lookup in our hash. 562 * Thus, tree traversals amount to a lookup in our hash.
515 * 563 *
516 * The tree itself is identified by the accessibility tree id of the 564 * The tree itself is identified by the accessibility tree id of the
517 * renderer widget host. 565 * renderer widget host.
518 * @constructor 566 * @constructor
519 */ 567 */
520 function AutomationRootNodeImpl(treeID) { 568 function AutomationRootNodeImpl(treeID) {
521 AutomationNodeImpl.call(this, this); 569 AutomationNodeImpl.call(this, this);
522 this.treeID = treeID; 570 this.treeID = treeID;
523 this.axNodeDataCache_ = {}; 571 this.axNodeDataCache_ = {};
524 } 572 }
525 573
574 AutomationRootNodeImpl.idToAutomationRootNode_ = {};
575
576 AutomationRootNodeImpl.get = function(treeID) {
577 var result = AutomationRootNodeImpl.idToAutomationRootNode_[treeID];
578 if (result)
579 return result;
580 return undefined;
581 }
582
583 AutomationRootNodeImpl.getOrCreate = function(treeID) {
584 if (AutomationRootNodeImpl.idToAutomationRootNode_[treeID])
585 return AutomationRootNodeImpl.idToAutomationRootNode_[treeID];
586 var result = new AutomationRootNode(treeID);
587 AutomationRootNodeImpl.idToAutomationRootNode_[treeID] = result;
588 return result;
589 }
590
591 AutomationRootNodeImpl.destroy = function(treeID) {
592 delete AutomationRootNodeImpl.idToAutomationRootNode_[treeID];
593 }
594
526 AutomationRootNodeImpl.prototype = { 595 AutomationRootNodeImpl.prototype = {
527 __proto__: AutomationNodeImpl.prototype, 596 __proto__: AutomationNodeImpl.prototype,
528 597
529 isRootNode: true, 598 isRootNode: true,
530 treeID: -1, 599 treeID: -1,
531 600
601 get id() {
602 return GetRootID(this.treeID);
603 },
604
532 get: function(id) { 605 get: function(id) {
533 if (id == undefined) 606 if (id == undefined)
534 return undefined; 607 return undefined;
535 608
536 return this.axNodeDataCache_[id]; 609 if (id == this.id)
537 }, 610 return this.wrapper;
538 611
539 unserialize: function(update) { 612 var obj = this.axNodeDataCache_[id];
540 var updateState = { pendingNodes: {}, newNodes: {} }; 613 if (obj)
541 var oldRootId = this.id; 614 return obj;
542 615
543 if (update.nodeIdToClear < 0) { 616 obj = new AutomationNode(this);
544 logging.WARNING('Bad nodeIdToClear: ' + update.nodeIdToClear); 617 privates(obj).impl.treeID = this.treeID;
545 lastError.set('automation', 618 privates(obj).impl.id = id;
546 'Bad update received on automation tree', 619 this.axNodeDataCache_[id] = obj;
547 null,
548 chrome);
549 return false;
550 } else if (update.nodeIdToClear > 0) {
551 var nodeToClear = this.axNodeDataCache_[update.nodeIdToClear];
552 if (!nodeToClear) {
553 logging.WARNING('Bad nodeIdToClear: ' + update.nodeIdToClear +
554 ' (not in cache)');
555 lastError.set('automation',
556 'Bad update received on automation tree',
557 null,
558 chrome);
559 return false;
560 }
561 if (nodeToClear === this.wrapper) {
562 this.invalidate_(nodeToClear);
563 } else {
564 var children = nodeToClear.children;
565 for (var i = 0; i < children.length; i++)
566 this.invalidate_(children[i]);
567 var nodeToClearImpl = privates(nodeToClear).impl;
568 nodeToClearImpl.childIds = []
569 updateState.pendingNodes[nodeToClearImpl.id] = nodeToClear;
570 }
571 }
572 620
573 for (var i = 0; i < update.nodes.length; i++) { 621 return obj;
574 if (!this.updateNode_(update.nodes[i], updateState))
575 return false;
576 }
577
578 if (Object.keys(updateState.pendingNodes).length > 0) {
579 logging.WARNING('Nodes left pending by the update: ' +
580 $JSON.stringify(updateState.pendingNodes));
581 lastError.set('automation',
582 'Bad update received on automation tree',
583 null,
584 chrome);
585 return false;
586 }
587
588 // Notify tree change observers of new nodes.
589 // TODO(dmazzoni): Notify tree change observers of changed nodes,
590 // and handle subtreeCreated and nodeCreated properly.
591 var observers = automationUtil.treeChangeObservers;
592 if (observers.length > 0) {
593 for (var nodeId in updateState.newNodes) {
594 var node = updateState.newNodes[nodeId];
595 var treeChange =
596 {target: node, type: schema.TreeChangeType.nodeCreated};
597 for (var i = 0; i < observers.length; i++) {
598 try {
599 observers[i](treeChange);
600 } catch (e) {
601 console.error('Error in tree change observer for ' +
602 treeChange.type + ': ' + e.message +
603 '\nStack trace: ' + e.stack);
604 }
605 }
606 }
607 }
608
609 return true;
610 }, 622 },
611 623
612 destroy: function() { 624 destroy: function() {
613 if (this.hostTree) 625 if (this.hostTree)
614 this.hostTree.childTree = undefined; 626 this.hostTree.childTree = undefined;
615 this.hostTree = undefined; 627 this.hostTree = undefined;
616 628
617 this.dispatchEvent(schema.EventType.destroyed); 629 this.dispatchEvent(schema.EventType.destroyed);
618 this.invalidate_(this.wrapper); 630 //this.invalidate_(this.wrapper);
619 }, 631 },
620 632
621 onAccessibilityEvent: function(eventParams) { 633 onAccessibilityEvent: function(eventParams) {
622 if (!this.unserialize(eventParams.update)) {
623 logging.WARNING('unserialization failed');
624 return false;
625 }
626
627 var targetNode = this.get(eventParams.targetID); 634 var targetNode = this.get(eventParams.targetID);
628 if (targetNode) { 635 if (targetNode) {
629 var targetNodeImpl = privates(targetNode).impl; 636 var targetNodeImpl = privates(targetNode).impl;
630 targetNodeImpl.dispatchEvent(eventParams.eventType); 637 targetNodeImpl.dispatchEvent(eventParams.eventType);
631 } else { 638 } else {
632 logging.WARNING('Got ' + eventParams.eventType + 639 logging.WARNING('Got ' + eventParams.eventType +
633 ' event on unknown node: ' + eventParams.targetID + 640 ' event on unknown node: ' + eventParams.targetID +
634 '; this: ' + this.id); 641 '; this: ' + this.id);
635 } 642 }
636 return true; 643 return true;
637 }, 644 },
638 645
639 toString: function() { 646 toString: function() {
640 function toStringInternal(node, indent) { 647 function toStringInternal(node, indent) {
641 if (!node) 648 if (!node)
642 return ''; 649 return '';
643 var output = 650 var output =
644 new Array(indent).join(' ') + 651 new Array(indent).join(' ') +
645 AutomationNodeImpl.prototype.toString.call(node) + 652 AutomationNodeImpl.prototype.toString.call(node) +
646 '\n'; 653 '\n';
647 indent += 2; 654 indent += 2;
648 for (var i = 0; i < node.children.length; i++) 655 for (var i = 0; i < node.children.length; i++)
649 output += toStringInternal(node.children[i], indent); 656 output += toStringInternal(node.children[i], indent);
650 return output; 657 return output;
651 } 658 }
652 return toStringInternal(this, 0); 659 return toStringInternal(this, 0);
653 }, 660 },
654
655 invalidate_: function(node) {
656 if (!node)
657 return;
658
659 // Notify tree change observers of the removed node.
660 var observers = automationUtil.treeChangeObservers;
661 if (observers.length > 0) {
662 var treeChange = {target: node, type: schema.TreeChangeType.nodeRemoved};
663 for (var i = 0; i < observers.length; i++) {
664 try {
665 observers[i](treeChange);
666 } catch (e) {
667 console.error('Error in tree change observer for ' + treeChange.type +
668 ': ' + e.message + '\nStack trace: ' + e.stack);
669 }
670 }
671 }
672
673 var children = node.children;
674
675 for (var i = 0, child; child = children[i]; i++) {
676 // Do not invalidate into subrooted nodes.
677 // TODO(dtseng): Revisit logic once out of proc iframes land.
678 if (child.root != node.root)
679 continue;
680 this.invalidate_(child);
681 }
682
683 // Retrieve the internal AutomationNodeImpl instance for this node.
684 // This object is not accessible outside of bindings code, but we can access
685 // it here.
686 var nodeImpl = privates(node).impl;
687 var id = nodeImpl.id;
688 for (var key in AutomationAttributeDefaults) {
689 nodeImpl[key] = AutomationAttributeDefaults[key];
690 }
691
692 nodeImpl.attributesInternal = {};
693 for (var key in DefaultMixinAttributes) {
694 var mixinAttribute = DefaultMixinAttributes[key];
695 if (!mixinAttribute.isRef)
696 nodeImpl.attributesInternal[key] = mixinAttribute.default;
697 }
698 nodeImpl.childIds = [];
699 nodeImpl.id = id;
700 delete this.axNodeDataCache_[id];
701 },
702
703 deleteOldChildren_: function(node, newChildIds) {
704 // Create a set of child ids in |src| for fast lookup, and return false
705 // if a duplicate is found;
706 var newChildIdSet = {};
707 for (var i = 0; i < newChildIds.length; i++) {
708 var childId = newChildIds[i];
709 if (newChildIdSet[childId]) {
710 logging.WARNING('Node ' + privates(node).impl.id +
711 ' has duplicate child id ' + childId);
712 lastError.set('automation',
713 'Bad update received on automation tree',
714 null,
715 chrome);
716 return false;
717 }
718 newChildIdSet[newChildIds[i]] = true;
719 }
720
721 // Delete the old children.
722 var nodeImpl = privates(node).impl;
723 var oldChildIds = nodeImpl.childIds;
724 for (var i = 0; i < oldChildIds.length;) {
725 var oldId = oldChildIds[i];
726 if (!newChildIdSet[oldId]) {
727 this.invalidate_(this.axNodeDataCache_[oldId]);
728 oldChildIds.splice(i, 1);
729 } else {
730 i++;
731 }
732 }
733 nodeImpl.childIds = oldChildIds;
734
735 return true;
736 },
737
738 createNewChildren_: function(node, newChildIds, updateState) {
739 logging.CHECK(node);
740 var success = true;
741
742 for (var i = 0; i < newChildIds.length; i++) {
743 var childId = newChildIds[i];
744 var childNode = this.axNodeDataCache_[childId];
745 if (childNode) {
746 if (childNode.parent != node) {
747 var parentId = -1;
748 if (childNode.parent) {
749 var parentImpl = privates(childNode.parent).impl;
750 parentId = parentImpl.id;
751 }
752 // This is a serious error - nodes should never be reparented.
753 // If this case occurs, continue so this node isn't left in an
754 // inconsistent state, but return failure at the end.
755 logging.WARNING('Node ' + childId + ' reparented from ' +
756 parentId + ' to ' + privates(node).impl.id);
757 lastError.set('automation',
758 'Bad update received on automation tree',
759 null,
760 chrome);
761 success = false;
762 continue;
763 }
764 } else {
765 childNode = new AutomationNode(this);
766 this.axNodeDataCache_[childId] = childNode;
767 privates(childNode).impl.id = childId;
768 updateState.pendingNodes[childId] = childNode;
769 updateState.newNodes[childId] = childNode;
770 }
771 privates(childNode).impl.indexInParent = i;
772 privates(childNode).impl.parentID = privates(node).impl.id;
773 }
774
775 return success;
776 },
777
778 setData_: function(node, nodeData) {
779 var nodeImpl = privates(node).impl;
780
781 // TODO(dtseng): Make into set listing all hosting node roles.
782 if (nodeData.role == schema.RoleType.webView) {
783 if (nodeImpl.childTreeID !== nodeData.intAttributes.childTreeId)
784 nodeImpl.pendingChildFrame = true;
785
786 if (nodeImpl.pendingChildFrame) {
787 nodeImpl.childTreeID = nodeData.intAttributes.childTreeId;
788 automationUtil.storeTreeCallback(nodeImpl.childTreeID, function(root) {
789 nodeImpl.pendingChildFrame = false;
790 nodeImpl.childTree = root;
791 privates(root).impl.hostTree = node;
792 if (root.attributes.docLoadingProgress == 1)
793 privates(root).impl.dispatchEvent(schema.EventType.loadComplete);
794 nodeImpl.dispatchEvent(schema.EventType.childrenChanged);
795 });
796 automationInternal.enableFrame(nodeImpl.childTreeID);
797 }
798 }
799 for (var key in AutomationAttributeDefaults) {
800 if (key in nodeData)
801 nodeImpl[key] = nodeData[key];
802 else
803 nodeImpl[key] = AutomationAttributeDefaults[key];
804 }
805
806 // Set all basic attributes.
807 this.mixinAttributes_(nodeImpl, DefaultMixinAttributes, nodeData);
808
809 // If this is a rootWebArea or webArea, set document attributes.
810 if (nodeData.role == schema.RoleType.rootWebArea ||
811 nodeData.role == schema.RoleType.webArea) {
812 this.mixinAttributes_(nodeImpl, DocumentMixinAttributes, nodeData);
813 }
814
815 // If this is a scrollable area, set scrollable attributes.
816 for (var scrollAttr in ScrollableMixinAttributes) {
817 var spec = ScrollableMixinAttributes[scrollAttr];
818 if (this.findAttribute_(scrollAttr, spec, nodeData) !== undefined) {
819 this.mixinAttributes_(nodeImpl, ScrollableMixinAttributes, nodeData);
820 break;
821 }
822 }
823
824 // If this is inside a live region, set live region mixins.
825 var attr = 'containerLiveStatus';
826 var spec = LiveRegionMixinAttributes[attr];
827 if (this.findAttribute_(attr, spec, nodeData) !== undefined) {
828 this.mixinAttributes_(nodeImpl, LiveRegionMixinAttributes, nodeData);
829 }
830
831 // If this is a link, set link attributes
832 if (nodeData.role == 'link') {
833 this.mixinAttributes_(nodeImpl, LinkMixinAttributes, nodeData);
834 }
835
836 // If this is an editable text area, set editable text attributes.
837 if (nodeData.role == schema.RoleType.textField ||
838 nodeData.role == schema.RoleType.spinButton) {
839 this.mixinAttributes_(nodeImpl, EditableTextMixinAttributes, nodeData);
840 }
841
842 // If this is a range type, set range attributes.
843 if (nodeData.role == schema.RoleType.progressIndicator ||
844 nodeData.role == schema.RoleType.scrollBar ||
845 nodeData.role == schema.RoleType.slider ||
846 nodeData.role == schema.RoleType.spinButton) {
847 this.mixinAttributes_(nodeImpl, RangeMixinAttributes, nodeData);
848 }
849
850 // If this is a table, set table attributes.
851 if (nodeData.role == schema.RoleType.table) {
852 this.mixinAttributes_(nodeImpl, TableMixinAttributes, nodeData);
853 }
854
855 // If this is a table cell, set table cell attributes.
856 if (nodeData.role == schema.RoleType.cell) {
857 this.mixinAttributes_(nodeImpl, TableCellMixinAttributes, nodeData);
858 }
859
860 // If this has an active descendant, expose it.
861 if ('intAttributes' in nodeData &&
862 'activedescendantId' in nodeData.intAttributes) {
863 this.mixinAttributes_(nodeImpl, ActiveDescendantMixinAttribute, nodeData);
864 }
865
866 for (var i = 0; i < AutomationAttributeTypes.length; i++) {
867 var attributeType = AutomationAttributeTypes[i];
868 for (var attributeName in nodeData[attributeType]) {
869 nodeImpl.attributesInternal[attributeName] =
870 nodeData[attributeType][attributeName];
871 if (ATTRIBUTE_BLACKLIST.hasOwnProperty(attributeName) ||
872 nodeImpl.attributes.hasOwnProperty(attributeName)) {
873 continue;
874 } else if (
875 ATTRIBUTE_NAME_TO_ID_ATTRIBUTE.hasOwnProperty(attributeName)) {
876 this.defineReadonlyAttribute_(nodeImpl,
877 nodeImpl.attributes,
878 attributeName,
879 true);
880 } else {
881 this.defineReadonlyAttribute_(nodeImpl,
882 nodeImpl.attributes,
883 attributeName);
884 }
885 }
886 }
887 },
888
889 mixinAttributes_: function(nodeImpl, attributes, nodeData) {
890 for (var attribute in attributes) {
891 var spec = attributes[attribute];
892 if (spec.isRef)
893 this.mixinRelationshipAttribute_(nodeImpl, attribute, spec, nodeData);
894 else
895 this.mixinAttribute_(nodeImpl, attribute, spec, nodeData);
896 }
897 },
898
899 mixinAttribute_: function(nodeImpl, attribute, spec, nodeData) {
900 var value = this.findAttribute_(attribute, spec, nodeData);
901 if (value === undefined)
902 value = spec.default;
903 nodeImpl.attributesInternal[attribute] = value;
904 this.defineReadonlyAttribute_(nodeImpl, nodeImpl, attribute);
905 },
906
907 mixinRelationshipAttribute_: function(nodeImpl, attribute, spec, nodeData) {
908 var idAttribute = spec.idAttribute;
909 var idValue = spec.default;
910 if (spec.idFrom in nodeData) {
911 idValue = idAttribute in nodeData[spec.idFrom]
912 ? nodeData[spec.idFrom][idAttribute] : idValue;
913 }
914
915 // Ok to define a list attribute with an empty list, but not a
916 // single ref with a null ID.
917 if (idValue === null)
918 return;
919
920 nodeImpl.attributesInternal[idAttribute] = idValue;
921 this.defineReadonlyAttribute_(
922 nodeImpl, nodeImpl, attribute, true, idAttribute);
923 },
924
925 findAttribute_: function(attribute, spec, nodeData) {
926 if (!('reflectFrom' in spec))
927 return;
928 var attributeGroup = spec.reflectFrom;
929 if (!(attributeGroup in nodeData))
930 return;
931
932 return nodeData[attributeGroup][attribute];
933 },
934
935 defineReadonlyAttribute_: function(
936 node, object, attributeName, opt_isIDRef, opt_idAttribute) {
937 if (attributeName in object)
938 return;
939
940 if (opt_isIDRef) {
941 $Object.defineProperty(object, attributeName, {
942 enumerable: true,
943 get: function() {
944 var idAttribute = opt_idAttribute ||
945 ATTRIBUTE_NAME_TO_ID_ATTRIBUTE[attributeName];
946 var idValue = node.attributesInternal[idAttribute];
947 if (Array.isArray(idValue)) {
948 return idValue.map(function(current) {
949 return node.rootImpl.get(current);
950 }, this);
951 }
952 return node.rootImpl.get(idValue);
953 }.bind(this),
954 });
955 } else {
956 $Object.defineProperty(object, attributeName, {
957 enumerable: true,
958 get: function() {
959 return node.attributesInternal[attributeName];
960 }.bind(this),
961 });
962 }
963
964 if (object instanceof AutomationNodeImpl) {
965 // Also expose attribute publicly on the wrapper.
966 $Object.defineProperty(object.wrapper, attributeName, {
967 enumerable: true,
968 get: function() {
969 return object[attributeName];
970 },
971 });
972
973 }
974 },
975
976 updateNode_: function(nodeData, updateState) {
977 var node = this.axNodeDataCache_[nodeData.id];
978 var didUpdateRoot = false;
979 if (node) {
980 delete updateState.pendingNodes[privates(node).impl.id];
981 } else {
982 if (nodeData.role != schema.RoleType.rootWebArea &&
983 nodeData.role != schema.RoleType.desktop) {
984 logging.WARNING(String(nodeData.id) +
985 ' is not in the cache and not the new root.');
986 lastError.set('automation',
987 'Bad update received on automation tree',
988 null,
989 chrome);
990 return false;
991 }
992 // |this| is an AutomationRootNodeImpl; retrieve the
993 // AutomationRootNode instance instead.
994 node = this.wrapper;
995 didUpdateRoot = true;
996 updateState.newNodes[this.id] = this.wrapper;
997 }
998 this.setData_(node, nodeData);
999
1000 // TODO(aboxhall): send onChanged event?
1001 logging.CHECK(node);
1002 if (!this.deleteOldChildren_(node, nodeData.childIds)) {
1003 if (didUpdateRoot) {
1004 this.invalidate_(this.wrapper);
1005 }
1006 return false;
1007 }
1008 var nodeImpl = privates(node).impl;
1009
1010 var success = this.createNewChildren_(node,
1011 nodeData.childIds,
1012 updateState);
1013 nodeImpl.childIds = nodeData.childIds;
1014 this.axNodeDataCache_[nodeImpl.id] = node;
1015
1016 return success;
1017 }
1018 }; 661 };
1019 662
1020
1021 var AutomationNode = utils.expose('AutomationNode', 663 var AutomationNode = utils.expose('AutomationNode',
1022 AutomationNodeImpl, 664 AutomationNodeImpl,
1023 { functions: ['doDefault', 665 { functions: ['doDefault',
1024 'find', 666 'find',
1025 'findAll', 667 'findAll',
1026 'focus', 668 'focus',
1027 'makeVisible', 669 'makeVisible',
1028 'matches', 670 'matches',
1029 'setSelection', 671 'setSelection',
1030 'showContextMenu', 672 'showContextMenu',
1031 'addEventListener', 673 'addEventListener',
1032 'removeEventListener', 674 'removeEventListener',
1033 'domQuerySelector', 675 'domQuerySelector',
1034 'toString' ], 676 'toString' ],
1035 readonly: ['parent', 677 readonly: publicAttributes.concat(
678 ['parent',
1036 'firstChild', 679 'firstChild',
1037 'lastChild', 680 'lastChild',
1038 'children', 681 'children',
1039 'previousSibling', 682 'previousSibling',
1040 'nextSibling', 683 'nextSibling',
1041 'isRootNode', 684 'isRootNode',
1042 'role', 685 'role',
1043 'state', 686 'state',
1044 'location', 687 'location',
1045 'attributes',
1046 'indexInParent', 688 'indexInParent',
1047 'root'] }); 689 'root']) });
1048 690
1049 var AutomationRootNode = utils.expose('AutomationRootNode', 691 var AutomationRootNode = utils.expose('AutomationRootNode',
1050 AutomationRootNodeImpl, 692 AutomationRootNodeImpl,
1051 { superclass: AutomationNode }); 693 { superclass: AutomationNode });
1052 694
695 AutomationRootNode.get = function(treeID) {
696 return AutomationRootNodeImpl.get(treeID);
697 }
698
699 AutomationRootNode.getOrCreate = function(treeID) {
700 return AutomationRootNodeImpl.getOrCreate(treeID);
701 }
702
703 AutomationRootNode.destroy = function(treeID) {
704 AutomationRootNodeImpl.destroy(treeID);
705 }
706
1053 exports.AutomationNode = AutomationNode; 707 exports.AutomationNode = AutomationNode;
1054 exports.AutomationRootNode = AutomationRootNode; 708 exports.AutomationRootNode = AutomationRootNode;
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698