blob: 709a45a60869679573cde1b9a28d8e0e0f6d0f36 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
var AutomationEvent = require('automationEvent').AutomationEvent;
var automationInternal =
require('binding').Binding.create('automationInternal').generate();
var IsInteractPermitted =
requireNative('automationInternal').IsInteractPermitted;
var lastError = require('lastError');
var logging = requireNative('logging');
var schema = requireNative('automationInternal').GetSchemaAdditions();
var utils = require('utils');
/**
* A single node in the Automation tree.
* @param {AutomationRootNodeImpl} root The root of the tree.
* @constructor
*/
function AutomationNodeImpl(root) {
this.rootImpl = root;
this.childIds = [];
// Public attributes. No actual data gets set on this object.
this.attributes = {};
// Internal object holding all attributes.
this.attributesInternal = {};
this.listeners = {};
this.location = { left: 0, top: 0, width: 0, height: 0 };
}
AutomationNodeImpl.prototype = {
id: -1,
role: '',
state: { busy: true },
isRootNode: false,
get root() {
return this.rootImpl.wrapper;
},
get parent() {
return this.hostTree || this.rootImpl.get(this.parentID);
},
get firstChild() {
return this.childTree || this.rootImpl.get(this.childIds[0]);
},
get lastChild() {
var childIds = this.childIds;
return this.childTree || this.rootImpl.get(childIds[childIds.length - 1]);
},
get children() {
if (this.childTree)
return [this.childTree];
var children = [];
for (var i = 0, childID; childID = this.childIds[i]; i++) {
logging.CHECK(this.rootImpl.get(childID));
children.push(this.rootImpl.get(childID));
}
return children;
},
get previousSibling() {
var parent = this.parent;
if (parent && this.indexInParent > 0)
return parent.children[this.indexInParent - 1];
return undefined;
},
get nextSibling() {
var parent = this.parent;
if (parent && this.indexInParent < parent.children.length)
return parent.children[this.indexInParent + 1];
return undefined;
},
doDefault: function() {
this.performAction_('doDefault');
},
focus: function() {
this.performAction_('focus');
},
makeVisible: function() {
this.performAction_('makeVisible');
},
setSelection: function(startIndex, endIndex) {
this.performAction_('setSelection',
{ startIndex: startIndex,
endIndex: endIndex });
},
domQuerySelector: function(selector, callback) {
automationInternal.querySelector(
{ treeID: this.rootImpl.treeID,
automationNodeID: this.id,
selector: selector },
this.domQuerySelectorCallback_.bind(this, callback));
},
find: function(params) {
return this.findInternal_(params);
},
findAll: function(params) {
return this.findInternal_(params, []);
},
matches: function(params) {
return this.matchInternal_(params);
},
addEventListener: function(eventType, callback, capture) {
this.removeEventListener(eventType, callback);
if (!this.listeners[eventType])
this.listeners[eventType] = [];
this.listeners[eventType].push({callback: callback, capture: !!capture});
},
// TODO(dtseng/aboxhall): Check this impl against spec.
removeEventListener: function(eventType, callback) {
if (this.listeners[eventType]) {
var listeners = this.listeners[eventType];
for (var i = 0; i < listeners.length; i++) {
if (callback === listeners[i].callback)
listeners.splice(i, 1);
}
}
},
toJSON: function() {
return { treeID: this.treeID,
id: this.id,
role: this.role,
attributes: this.attributes };
},
dispatchEvent: function(eventType) {
var path = [];
var parent = this.parent;
while (parent) {
path.push(parent);
parent = parent.parent;
}
var event = new AutomationEvent(eventType, this.wrapper);
// Dispatch the event through the propagation path in three phases:
// - capturing: starting from the root and going down to the target's parent
// - targeting: dispatching the event on the target itself
// - bubbling: starting from the target's parent, going back up to the root.
// At any stage, a listener may call stopPropagation() on the event, which
// will immediately stop event propagation through this path.
if (this.dispatchEventAtCapturing_(event, path)) {
if (this.dispatchEventAtTargeting_(event, path))
this.dispatchEventAtBubbling_(event, path);
}
},
toString: function() {
var impl = privates(this).impl;
if (!impl)
impl = this;
return 'node id=' + impl.id +
' role=' + this.role +
' state=' + $JSON.stringify(this.state) +
' parentID=' + impl.parentID +
' childIds=' + $JSON.stringify(impl.childIds) +
' attributes=' + $JSON.stringify(this.attributes);
},
dispatchEventAtCapturing_: function(event, path) {
privates(event).impl.eventPhase = Event.CAPTURING_PHASE;
for (var i = path.length - 1; i >= 0; i--) {
this.fireEventListeners_(path[i], event);
if (privates(event).impl.propagationStopped)
return false;
}
return true;
},
dispatchEventAtTargeting_: function(event) {
privates(event).impl.eventPhase = Event.AT_TARGET;
this.fireEventListeners_(this.wrapper, event);
return !privates(event).impl.propagationStopped;
},
dispatchEventAtBubbling_: function(event, path) {
privates(event).impl.eventPhase = Event.BUBBLING_PHASE;
for (var i = 0; i < path.length; i++) {
this.fireEventListeners_(path[i], event);
if (privates(event).impl.propagationStopped)
return false;
}
return true;
},
fireEventListeners_: function(node, event) {
var nodeImpl = privates(node).impl;
var listeners = nodeImpl.listeners[event.type];
if (!listeners)
return;
var eventPhase = event.eventPhase;
for (var i = 0; i < listeners.length; i++) {
if (eventPhase == Event.CAPTURING_PHASE && !listeners[i].capture)
continue;
if (eventPhase == Event.BUBBLING_PHASE && listeners[i].capture)
continue;
try {
listeners[i].callback(event);
} catch (e) {
console.error('Error in event handler for ' + event.type +
'during phase ' + eventPhase + ': ' +
e.message + '\nStack trace: ' + e.stack);
}
}
},
performAction_: function(actionType, opt_args) {
// Not yet initialized.
if (this.rootImpl.treeID === undefined ||
this.id === undefined) {
return;
}
// Check permissions.
if (!IsInteractPermitted()) {
throw new Error(actionType + ' requires {"desktop": true} or' +
' {"interact": true} in the "automation" manifest key.');
}
automationInternal.performAction({ treeID: this.rootImpl.treeID,
automationNodeID: this.id,
actionType: actionType },
opt_args || {});
},
domQuerySelectorCallback_: function(userCallback, resultAutomationNodeID) {
// resultAutomationNodeID could be zero or undefined or (unlikely) null;
// they all amount to the same thing here, which is that no node was
// returned.
if (!resultAutomationNodeID) {
userCallback(null);
return;
}
var resultNode = this.rootImpl.get(resultAutomationNodeID);
if (!resultNode) {
logging.WARNING('Query selector result not in tree: ' +
resultAutomationNodeID);
userCallback(null);
}
userCallback(resultNode);
},
findInternal_: function(params, opt_results) {
var result = null;
this.forAllDescendants_(function(node) {
if (privates(node).impl.matchInternal_(params)) {
if (opt_results)
opt_results.push(node);
else
result = node;
return !opt_results;
}
});
if (opt_results)
return opt_results;
return result;
},
/**
* Executes a closure for all of this node's descendants, in pre-order.
* Early-outs if the closure returns true.
* @param {Function(AutomationNode):boolean} closure Closure to be executed
* for each node. Return true to early-out the traversal.
*/
forAllDescendants_: function(closure) {
var stack = this.wrapper.children.reverse();
while (stack.length > 0) {
var node = stack.pop();
if (closure(node))
return;
var children = node.children;
for (var i = children.length - 1; i >= 0; i--)
stack.push(children[i]);
}
},
matchInternal_: function(params) {
if (Object.keys(params).length == 0)
return false;
if ('role' in params && this.role != params.role)
return false;
if ('state' in params) {
for (var state in params.state) {
if (params.state[state] != (state in this.state))
return false;
}
}
if ('attributes' in params) {
for (var attribute in params.attributes) {
if (!(attribute in this.attributesInternal))
return false;
var attrValue = params.attributes[attribute];
if (typeof attrValue != 'object') {
if (this.attributesInternal[attribute] !== attrValue)
return false;
} else if (attrValue instanceof RegExp) {
if (typeof this.attributesInternal[attribute] != 'string')
return false;
if (!attrValue.test(this.attributesInternal[attribute]))
return false;
} else {
// TODO(aboxhall): handle intlist case.
return false;
}
}
}
return true;
}
};
// Maps an attribute to its default value in an invalidated node.
// These attributes are taken directly from the Automation idl.
var AutomationAttributeDefaults = {
'id': -1,
'role': '',
'state': {},
'location': { left: 0, top: 0, width: 0, height: 0 }
};
var AutomationAttributeTypes = [
'boolAttributes',
'floatAttributes',
'htmlAttributes',
'intAttributes',
'intlistAttributes',
'stringAttributes'
];
/**
* Maps an attribute name to another attribute who's value is an id or an array
* of ids referencing an AutomationNode.
* @param {!Object<string, string>}
* @const
*/
var ATTRIBUTE_NAME_TO_ID_ATTRIBUTE = {
'aria-activedescendant': 'activedescendantId',
'aria-controls': 'controlsIds',
'aria-describedby': 'describedbyIds',
'aria-flowto': 'flowtoIds',
'aria-labelledby': 'labelledbyIds',
'aria-owns': 'ownsIds'
};
/**
* A set of attributes ignored in the automation API.
* @param {!Object<string, boolean>}
* @const
*/
var ATTRIBUTE_BLACKLIST = {'activedescendantId': true,
'childTreeId': true,
'controlsIds': true,
'describedbyIds': true,
'flowtoIds': true,
'labelledbyIds': true,
'ownsIds': true
};
function defaultStringAttribute(opt_defaultVal) {
var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : '';
return { default: defaultVal, reflectFrom: 'stringAttributes' };
}
function defaultIntAttribute(opt_defaultVal) {
var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : 0;
return { default: defaultVal, reflectFrom: 'intAttributes' };
}
function defaultFloatAttribute(opt_defaultVal) {
var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : 0;
return { default: defaultVal, reflectFrom: 'floatAttributes' };
}
function defaultBoolAttribute(opt_defaultVal) {
var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : false;
return { default: defaultVal, reflectFrom: 'boolAttributes' };
}
function defaultHtmlAttribute(opt_defaultVal) {
var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : '';
return { default: defaultVal, reflectFrom: 'htmlAttributes' };
}
function defaultIntListAttribute(opt_defaultVal) {
var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : [];
return { default: defaultVal, reflectFrom: 'intlistAttributes' };
}
function defaultNodeRefAttribute(idAttribute, opt_defaultVal) {
var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : null;
return { default: defaultVal,
idFrom: 'intAttributes',
idAttribute: idAttribute,
isRef: true };
}
function defaultNodeRefListAttribute(idAttribute, opt_defaultVal) {
var defaultVal = (opt_defaultVal !== undefined) ? opt_defaultVal : [];
return { default: [],
idFrom: 'intlistAttributes',
idAttribute: idAttribute,
isRef: true };
}
// Maps an attribute to its default value in an invalidated node.
// These attributes are taken directly from the Automation idl.
var DefaultMixinAttributes = {
description: defaultStringAttribute(),
help: defaultStringAttribute(),
name: defaultStringAttribute(),
value: defaultStringAttribute(),
controls: defaultNodeRefListAttribute('controlsIds'),
describedby: defaultNodeRefListAttribute('describedbyIds'),
flowto: defaultNodeRefListAttribute('flowtoIds'),
labelledby: defaultNodeRefListAttribute('labelledbyIds'),
owns: defaultNodeRefListAttribute('ownsIds')
};
var ActiveDescendantMixinAttribute = {
activedescendant: defaultNodeRefAttribute('activedescendantId')
};
var LinkMixinAttributes = {
url: defaultStringAttribute()
};
var DocumentMixinAttributes = {
docUrl: defaultStringAttribute(),
docTitle: defaultStringAttribute(),
docLoaded: defaultStringAttribute(),
docLoadingProgress: defaultFloatAttribute()
};
var ScrollableMixinAttributes = {
scrollX: defaultIntAttribute(),
scrollXMin: defaultIntAttribute(),
scrollXMax: defaultIntAttribute(),
scrollY: defaultIntAttribute(),
scrollYMin: defaultIntAttribute(),
scrollYMax: defaultIntAttribute()
};
var EditableTextMixinAttributes = {
textSelStart: defaultIntAttribute(-1),
textSelEnd: defaultIntAttribute(-1)
};
var RangeMixinAttributes = {
valueForRange: defaultFloatAttribute(),
minValueForRange: defaultFloatAttribute(),
maxValueForRange: defaultFloatAttribute()
};
var TableMixinAttributes = {
tableRowCount: defaultIntAttribute(),
tableColumnCount: defaultIntAttribute()
};
var TableCellMixinAttributes = {
tableCellColumnIndex: defaultIntAttribute(),
tableCellColumnSpan: defaultIntAttribute(1),
tableCellRowIndex: defaultIntAttribute(),
tableCellRowSpan: defaultIntAttribute(1)
};
/**
* AutomationRootNode.
*
* An AutomationRootNode is the javascript end of an AXTree living in the
* browser. AutomationRootNode handles unserializing incremental updates from
* the source AXTree. Each update contains node data that form a complete tree
* after applying the update.
*
* A brief note about ids used through this class. The source AXTree assigns
* unique ids per node and we use these ids to build a hash to the actual
* AutomationNode object.
* Thus, tree traversals amount to a lookup in our hash.
*
* The tree itself is identified by the accessibility tree id of the
* renderer widget host.
* @constructor
*/
function AutomationRootNodeImpl(treeID) {
AutomationNodeImpl.call(this, this);
this.treeID = treeID;
this.axNodeDataCache_ = {};
}
AutomationRootNodeImpl.prototype = {
__proto__: AutomationNodeImpl.prototype,
isRootNode: true,
treeID: -1,
get: function(id) {
if (id == undefined)
return undefined;
return this.axNodeDataCache_[id];
},
unserialize: function(update) {
var updateState = { pendingNodes: {}, newNodes: {} };
var oldRootId = this.id;
if (update.nodeIdToClear < 0) {
logging.WARNING('Bad nodeIdToClear: ' + update.nodeIdToClear);
lastError.set('automation',
'Bad update received on automation tree',
null,
chrome);
return false;
} else if (update.nodeIdToClear > 0) {
var nodeToClear = this.axNodeDataCache_[update.nodeIdToClear];
if (!nodeToClear) {
logging.WARNING('Bad nodeIdToClear: ' + update.nodeIdToClear +
' (not in cache)');
lastError.set('automation',
'Bad update received on automation tree',
null,
chrome);
return false;
}
if (nodeToClear === this.wrapper) {
this.invalidate_(nodeToClear);
} else {
var children = nodeToClear.children;
for (var i = 0; i < children.length; i++)
this.invalidate_(children[i]);
var nodeToClearImpl = privates(nodeToClear).impl;
nodeToClearImpl.childIds = []
updateState.pendingNodes[nodeToClearImpl.id] = nodeToClear;
}
}
for (var i = 0; i < update.nodes.length; i++) {
if (!this.updateNode_(update.nodes[i], updateState))
return false;
}
if (Object.keys(updateState.pendingNodes).length > 0) {
logging.WARNING('Nodes left pending by the update: ' +
$JSON.stringify(updateState.pendingNodes));
lastError.set('automation',
'Bad update received on automation tree',
null,
chrome);
return false;
}
return true;
},
destroy: function() {
if (this.hostTree)
this.hostTree.childTree = undefined;
this.hostTree = undefined;
this.dispatchEvent(schema.EventType.destroyed);
this.invalidate_(this.wrapper);
},
onAccessibilityEvent: function(eventParams) {
if (!this.unserialize(eventParams.update)) {
logging.WARNING('unserialization failed');
return false;
}
var targetNode = this.get(eventParams.targetID);
if (targetNode) {
var targetNodeImpl = privates(targetNode).impl;
targetNodeImpl.dispatchEvent(eventParams.eventType);
} else {
logging.WARNING('Got ' + eventParams.eventType +
' event on unknown node: ' + eventParams.targetID +
'; this: ' + this.id);
}
return true;
},
toString: function() {
function toStringInternal(node, indent) {
if (!node)
return '';
var output =
new Array(indent).join(' ') +
AutomationNodeImpl.prototype.toString.call(node) +
'\n';
indent += 2;
for (var i = 0; i < node.children.length; i++)
output += toStringInternal(node.children[i], indent);
return output;
}
return toStringInternal(this, 0);
},
invalidate_: function(node) {
if (!node)
return;
var children = node.children;
for (var i = 0, child; child = children[i]; i++) {
// Do not invalidate into subrooted nodes.
// TODO(dtseng): Revisit logic once out of proc iframes land.
if (child.root != node.root)
continue;
this.invalidate_(child);
}
// Retrieve the internal AutomationNodeImpl instance for this node.
// This object is not accessible outside of bindings code, but we can access
// it here.
var nodeImpl = privates(node).impl;
var id = nodeImpl.id;
for (var key in AutomationAttributeDefaults) {
nodeImpl[key] = AutomationAttributeDefaults[key];
}
nodeImpl.attributesInternal = {};
for (var key in DefaultMixinAttributes) {
var mixinAttribute = DefaultMixinAttributes[key];
if (!mixinAttribute.isRef)
nodeImpl.attributesInternal[key] = mixinAttribute.default;
}
nodeImpl.childIds = [];
nodeImpl.id = id;
delete this.axNodeDataCache_[id];
},
deleteOldChildren_: function(node, newChildIds) {
// Create a set of child ids in |src| for fast lookup, and return false
// if a duplicate is found;
var newChildIdSet = {};
for (var i = 0; i < newChildIds.length; i++) {
var childId = newChildIds[i];
if (newChildIdSet[childId]) {
logging.WARNING('Node ' + privates(node).impl.id +
' has duplicate child id ' + childId);
lastError.set('automation',
'Bad update received on automation tree',
null,
chrome);
return false;
}
newChildIdSet[newChildIds[i]] = true;
}
// Delete the old children.
var nodeImpl = privates(node).impl;
var oldChildIds = nodeImpl.childIds;
for (var i = 0; i < oldChildIds.length;) {
var oldId = oldChildIds[i];
if (!newChildIdSet[oldId]) {
this.invalidate_(this.axNodeDataCache_[oldId]);
oldChildIds.splice(i, 1);
} else {
i++;
}
}
nodeImpl.childIds = oldChildIds;
return true;
},
createNewChildren_: function(node, newChildIds, updateState) {
logging.CHECK(node);
var success = true;
for (var i = 0; i < newChildIds.length; i++) {
var childId = newChildIds[i];
var childNode = this.axNodeDataCache_[childId];
if (childNode) {
if (childNode.parent != node) {
var parentId = -1;
if (childNode.parent) {
var parentImpl = privates(childNode.parent).impl;
parentId = parentImpl.id;
}
// This is a serious error - nodes should never be reparented.
// If this case occurs, continue so this node isn't left in an
// inconsistent state, but return failure at the end.
logging.WARNING('Node ' + childId + ' reparented from ' +
parentId + ' to ' + privates(node).impl.id);
lastError.set('automation',
'Bad update received on automation tree',
null,
chrome);
success = false;
continue;
}
} else {
childNode = new AutomationNode(this);
this.axNodeDataCache_[childId] = childNode;
privates(childNode).impl.id = childId;
updateState.pendingNodes[childId] = childNode;
updateState.newNodes[childId] = childNode;
}
privates(childNode).impl.indexInParent = i;
privates(childNode).impl.parentID = privates(node).impl.id;
}
return success;
},
setData_: function(node, nodeData) {
var nodeImpl = privates(node).impl;
// TODO(dtseng): Make into set listing all hosting node roles.
if (nodeData.role == schema.RoleType.webView) {
if (nodeImpl.pendingChildFrame === undefined)
nodeImpl.pendingChildFrame = true;
if (nodeImpl.pendingChildFrame) {
nodeImpl.childTreeID = nodeData.intAttributes.childTreeId;
automationInternal.enableFrame(nodeImpl.childTreeID);
automationUtil.storeTreeCallback(nodeImpl.childTreeID, function(root) {
nodeImpl.pendingChildFrame = false;
nodeImpl.childTree = root;
privates(root).impl.hostTree = node;
nodeImpl.dispatchEvent(schema.EventType.childrenChanged);
});
}
}
for (var key in AutomationAttributeDefaults) {
if (key in nodeData)
nodeImpl[key] = nodeData[key];
else
nodeImpl[key] = AutomationAttributeDefaults[key];
}
// Set all basic attributes.
this.mixinAttributes_(nodeImpl, DefaultMixinAttributes, nodeData);
// If this is a rootWebArea or webArea, set document attributes.
if (nodeData.role == schema.RoleType.rootWebArea ||
nodeData.role == schema.RoleType.webArea) {
this.mixinAttributes_(nodeImpl, DocumentMixinAttributes, nodeData);
}
// If this is a scrollable area, set scrollable attributes.
for (var scrollAttr in ScrollableMixinAttributes) {
var spec = ScrollableMixinAttributes[scrollAttr];
if (this.findAttribute_(scrollAttr, spec, nodeData) !== undefined) {
this.mixinAttributes_(nodeImpl, ScrollableMixinAttributes, nodeData);
break;
}
}
// If this is a link, set link attributes
if (nodeData.role == 'link') {
this.mixinAttributes_(nodeImpl, LinkMixinAttributes, nodeData);
}
// If this is an editable text area, set editable text attributes.
if (nodeData.role == schema.RoleType.textField ||
nodeData.role == schema.RoleType.textArea) {
this.mixinAttributes_(nodeImpl, EditableTextMixinAttributes, nodeData);
}
// If this is a range type, set range attributes.
if (nodeData.role == schema.RoleType.progressIndicator ||
nodeData.role == schema.RoleType.scrollBar ||
nodeData.role == schema.RoleType.slider ||
nodeData.role == schema.RoleType.spinButton) {
this.mixinAttributes_(nodeImpl, RangeMixinAttributes, nodeData);
}
// If this is a table, set table attributes.
if (nodeData.role == schema.RoleType.table) {
this.mixinAttributes_(nodeImpl, TableMixinAttributes, nodeData);
}
// If this is a table cell, set table cell attributes.
if (nodeData.role == schema.RoleType.cell) {
this.mixinAttributes_(nodeImpl, TableCellMixinAttributes, nodeData);
}
// If this has an active descendant, expose it.
if ('intAttributes' in nodeData &&
'activedescendantId' in nodeData.intAttributes) {
this.mixinAttributes_(nodeImpl, ActiveDescendantMixinAttribute, nodeData);
}
for (var i = 0; i < AutomationAttributeTypes.length; i++) {
var attributeType = AutomationAttributeTypes[i];
for (var attributeName in nodeData[attributeType]) {
nodeImpl.attributesInternal[attributeName] =
nodeData[attributeType][attributeName];
if (ATTRIBUTE_BLACKLIST.hasOwnProperty(attributeName) ||
nodeImpl.attributes.hasOwnProperty(attributeName)) {
continue;
} else if (
ATTRIBUTE_NAME_TO_ID_ATTRIBUTE.hasOwnProperty(attributeName)) {
this.defineReadonlyAttribute_(nodeImpl,
nodeImpl.attributes,
attributeName,
true);
} else {
this.defineReadonlyAttribute_(nodeImpl,
nodeImpl.attributes,
attributeName);
}
}
}
},
mixinAttributes_: function(nodeImpl, attributes, nodeData) {
for (var attribute in attributes) {
var spec = attributes[attribute];
if (spec.isRef)
this.mixinRelationshipAttribute_(nodeImpl, attribute, spec, nodeData);
else
this.mixinAttribute_(nodeImpl, attribute, spec, nodeData);
}
},
mixinAttribute_: function(nodeImpl, attribute, spec, nodeData) {
var value = this.findAttribute_(attribute, spec, nodeData);
if (value === undefined)
value = spec.default;
nodeImpl.attributesInternal[attribute] = value;
this.defineReadonlyAttribute_(nodeImpl, nodeImpl, attribute);
},
mixinRelationshipAttribute_: function(nodeImpl, attribute, spec, nodeData) {
var idAttribute = spec.idAttribute;
var idValue = spec.default;
if (spec.idFrom in nodeData) {
idValue = idAttribute in nodeData[spec.idFrom]
? nodeData[spec.idFrom][idAttribute] : idValue;
}
// Ok to define a list attribute with an empty list, but not a
// single ref with a null ID.
if (idValue === null)
return;
nodeImpl.attributesInternal[idAttribute] = idValue;
this.defineReadonlyAttribute_(
nodeImpl, nodeImpl, attribute, true, idAttribute);
},
findAttribute_: function(attribute, spec, nodeData) {
if (!('reflectFrom' in spec))
return;
var attributeGroup = spec.reflectFrom;
if (!(attributeGroup in nodeData))
return;
return nodeData[attributeGroup][attribute];
},
defineReadonlyAttribute_: function(
node, object, attributeName, opt_isIDRef, opt_idAttribute) {
if (attributeName in object)
return;
if (opt_isIDRef) {
$Object.defineProperty(object, attributeName, {
enumerable: true,
get: function() {
var idAttribute = opt_idAttribute ||
ATTRIBUTE_NAME_TO_ID_ATTRIBUTE[attributeName];
var idValue = node.attributesInternal[idAttribute];
if (Array.isArray(idValue)) {
return idValue.map(function(current) {
return node.rootImpl.get(current);
}, this);
}
return node.rootImpl.get(idValue);
}.bind(this),
});
} else {
$Object.defineProperty(object, attributeName, {
enumerable: true,
get: function() {
return node.attributesInternal[attributeName];
}.bind(this),
});
}
if (object instanceof AutomationNodeImpl) {
// Also expose attribute publicly on the wrapper.
$Object.defineProperty(object.wrapper, attributeName, {
enumerable: true,
get: function() {
return object[attributeName];
},
});
}
},
updateNode_: function(nodeData, updateState) {
var node = this.axNodeDataCache_[nodeData.id];
var didUpdateRoot = false;
if (node) {
delete updateState.pendingNodes[privates(node).impl.id];
} else {
if (nodeData.role != schema.RoleType.rootWebArea &&
nodeData.role != schema.RoleType.desktop) {
logging.WARNING(String(nodeData.id) +
' is not in the cache and not the new root.');
lastError.set('automation',
'Bad update received on automation tree',
null,
chrome);
return false;
}
// |this| is an AutomationRootNodeImpl; retrieve the
// AutomationRootNode instance instead.
node = this.wrapper;
didUpdateRoot = true;
updateState.newNodes[this.id] = this.wrapper;
}
this.setData_(node, nodeData);
// TODO(aboxhall): send onChanged event?
logging.CHECK(node);
if (!this.deleteOldChildren_(node, nodeData.childIds)) {
if (didUpdateRoot) {
this.invalidate_(this.wrapper);
}
return false;
}
var nodeImpl = privates(node).impl;
var success = this.createNewChildren_(node,
nodeData.childIds,
updateState);
nodeImpl.childIds = nodeData.childIds;
this.axNodeDataCache_[nodeImpl.id] = node;
return success;
}
};
var AutomationNode = utils.expose('AutomationNode',
AutomationNodeImpl,
{ functions: ['doDefault',
'find',
'findAll',
'focus',
'makeVisible',
'matches',
'setSelection',
'addEventListener',
'removeEventListener',
'domQuerySelector',
'toJSON' ],
readonly: ['parent',
'firstChild',
'lastChild',
'children',
'previousSibling',
'nextSibling',
'isRootNode',
'role',
'state',
'location',
'attributes',
'indexInParent',
'root'] });
var AutomationRootNode = utils.expose('AutomationRootNode',
AutomationRootNodeImpl,
{ superclass: AutomationNode });
exports.AutomationNode = AutomationNode;
exports.AutomationRootNode = AutomationRootNode;