blob: 50b5c894e78c41e0a78cd026ab84bab9ba3994fc [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.
// Custom bindings for the automation API.
var AutomationNode = require('automationNode').AutomationNode;
var AutomationRootNode = require('automationNode').AutomationRootNode;
var automation = apiBridge || require('binding').Binding.create('automation');
var automationInternal =
getInternalApi ?
getInternalApi('automationInternal') :
require('binding').Binding.create('automationInternal').generate();
var exceptionHandler = require('uncaught_exception_handler');
var logging = requireNative('logging');
var nativeAutomationInternal = requireNative('automationInternal');
var DestroyAccessibilityTree =
nativeAutomationInternal.DestroyAccessibilityTree;
var GetIntAttribute = nativeAutomationInternal.GetIntAttribute;
var StartCachingAccessibilityTrees =
nativeAutomationInternal.StartCachingAccessibilityTrees;
var AddTreeChangeObserver = nativeAutomationInternal.AddTreeChangeObserver;
var RemoveTreeChangeObserver =
nativeAutomationInternal.RemoveTreeChangeObserver;
var GetFocusNative = nativeAutomationInternal.GetFocus;
var jsLastError = bindingUtil ? undefined : require('lastError');
function hasLastError() {
return bindingUtil ?
bindingUtil.hasLastError() : jsLastError.hasError(chrome);
}
/**
* A namespace to export utility functions to other files in automation.
*/
window.automationUtil = function() {};
// TODO(aboxhall): Look into using WeakMap
var idToCallback = {};
var DESKTOP_TREE_ID = "0";
automationUtil.storeTreeCallback = function(id, callback) {
if (!callback)
return;
var targetTree = AutomationRootNode.get(id);
if (!targetTree) {
// If we haven't cached the tree, hold the callback until the tree is
// populated by the initial onAccessibilityEvent call.
if (id in idToCallback)
idToCallback[id].push(callback);
else
idToCallback[id] = [callback];
} else {
callback(targetTree);
}
};
/**
* Global list of tree change observers.
* @type {Object<number, TreeChangeObserver>}
*/
automationUtil.treeChangeObserverMap = {};
/**
* The id of the next tree change observer.
* @type {number}
*/
automationUtil.nextTreeChangeObserverId = 1;
/**
* @type {AutomationNode} The current focused node. This is only updated
* when calling automationUtil.updateFocusedNode.
*/
automationUtil.focusedNode = null;
/**
* Gets the currently focused AutomationNode.
* @return {AutomationNode}
*/
automationUtil.getFocus = function() {
var focusedNodeInfo = GetFocusNative(DESKTOP_TREE_ID);
if (!focusedNodeInfo)
return null;
var tree = AutomationRootNode.getOrCreate(focusedNodeInfo.treeId);
if (tree)
return privates(tree).impl.get(focusedNodeInfo.nodeId);
};
/**
* Update automationUtil.focusedNode to be the node that currently has focus.
*/
automationUtil.updateFocusedNode = function() {
automationUtil.focusedNode = automationUtil.getFocus();
};
/**
* Updates the focus on blur.
*/
automationUtil.updateFocusedNodeOnBlur = function() {
var focus = automationUtil.getFocus();
automationUtil.focusedNode = focus ? focus.root : null;
};
automation.registerCustomHook(function(bindingsAPI) {
var apiFunctions = bindingsAPI.apiFunctions;
// TODO(aboxhall, dtseng): Make this return the speced AutomationRootNode obj.
apiFunctions.setHandleRequest('getTree', function getTree(tabID, callback) {
StartCachingAccessibilityTrees();
// enableTab() ensures the renderer for the active or specified tab has
// accessibility enabled, and fetches its ax tree id to use as
// a key in the idToAutomationRootNode map. The callback to
// enableTab is bound to the callback passed in to getTree(), so that once
// the tree is available (either due to having been cached earlier, or after
// an accessibility event occurs which causes the tree to be populated), the
// callback can be called.
var params = { tabID: tabID };
automationInternal.enableTab(params,
function onEnable(id) {
if (hasLastError()) {
callback();
return;
}
automationUtil.storeTreeCallback(id, callback);
});
});
var desktopTree = null;
apiFunctions.setHandleRequest('getDesktop', function(callback) {
StartCachingAccessibilityTrees();
desktopTree = AutomationRootNode.get(DESKTOP_TREE_ID);
if (!desktopTree) {
if (DESKTOP_TREE_ID in idToCallback)
idToCallback[DESKTOP_TREE_ID].push(callback);
else
idToCallback[DESKTOP_TREE_ID] = [callback];
// TODO(dtseng): Disable desktop tree once desktop object goes out of
// scope.
automationInternal.enableDesktop(function() {
if (hasLastError()) {
AutomationRootNode.destroy(DESKTOP_TREE_ID);
callback();
return;
}
});
} else {
callback(desktopTree);
}
});
apiFunctions.setHandleRequest('getFocus', function(callback) {
callback(automationUtil.getFocus());
});
function removeTreeChangeObserver(observer) {
for (var id in automationUtil.treeChangeObserverMap) {
if (automationUtil.treeChangeObserverMap[id] == observer) {
RemoveTreeChangeObserver(id);
delete automationUtil.treeChangeObserverMap[id];
return;
}
}
}
apiFunctions.setHandleRequest('removeTreeChangeObserver', function(observer) {
removeTreeChangeObserver(observer);
});
function addTreeChangeObserver(filter, observer) {
removeTreeChangeObserver(observer);
var id = automationUtil.nextTreeChangeObserverId++;
AddTreeChangeObserver(id, filter);
automationUtil.treeChangeObserverMap[id] = observer;
}
apiFunctions.setHandleRequest('addTreeChangeObserver',
function(filter, observer) {
addTreeChangeObserver(filter, observer);
});
apiFunctions.setHandleRequest('setDocumentSelection', function(params) {
var anchorNodeImpl = privates(params.anchorObject).impl;
var focusNodeImpl = privates(params.focusObject).impl;
if (anchorNodeImpl.treeID !== focusNodeImpl.treeID)
throw new Error('Selection anchor and focus must be in the same tree.');
if (anchorNodeImpl.treeID === DESKTOP_TREE_ID) {
throw new Error('Use AutomationNode.setSelection to set the selection ' +
'in the desktop tree.');
}
automationInternal.performAction({ treeID: anchorNodeImpl.treeID,
automationNodeID: anchorNodeImpl.id,
actionType: 'setSelection'},
{ focusNodeID: focusNodeImpl.id,
anchorOffset: params.anchorOffset,
focusOffset: params.focusOffset });
});
});
automationInternal.onChildTreeID.addListener(function(childTreeId) {
var targetTree = AutomationRootNode.get(childTreeId);
// If the tree is already loded, or if we previously requested it be loaded
// (i.e. have a callback for it), don't try to do so again.
if (targetTree || idToCallback[childTreeId])
return;
// A WebView in the desktop tree has a different AX tree as its child.
// When we encounter a WebView with a child AX tree id that we don't
// currently have cached, explicitly request that AX tree from the
// browser process and set up a callback when it loads to attach that
// tree as a child of this node and fire appropriate events.
automationUtil.storeTreeCallback(childTreeId, function(root) {
privates(root).impl.dispatchEvent('loadComplete', 'page');
}, true);
automationInternal.enableFrame(childTreeId);
});
automationInternal.onTreeChange.addListener(function(observerID,
treeID,
nodeID,
changeType) {
var tree = AutomationRootNode.getOrCreate(treeID);
if (!tree)
return;
var node = privates(tree).impl.get(nodeID);
if (!node)
return;
var observer = automationUtil.treeChangeObserverMap[observerID];
if (!observer)
return;
try {
observer({target: node, type: changeType});
} catch (e) {
exceptionHandler.handle('Error in tree change observer for ' +
changeType, e);
}
});
automationInternal.onNodesRemoved.addListener(function(treeID, nodeIDs) {
var tree = AutomationRootNode.getOrCreate(treeID);
if (!tree)
return;
for (var i = 0; i < nodeIDs.length; i++) {
privates(tree).impl.remove(nodeIDs[i]);
}
});
/**
* Dispatch accessibility events fired on individual nodes to its
* corresponding AutomationNode. Handle focus events specially
* (see below).
*/
automationInternal.onAccessibilityEvent.addListener(function(eventParams) {
var id = eventParams.treeID;
var targetTree = AutomationRootNode.getOrCreate(id);
if (eventParams.eventType == 'blur') {
// Work around an issue where Chrome sends us 'blur' events on the
// root node when nothing has focus, we need to treat those as focus
// events but otherwise not handle blur events specially.
var node = privates(targetTree).impl.get(eventParams.targetID);
if (node == node.root)
automationUtil.updateFocusedNodeOnBlur();
} else if (eventParams.eventType == 'mediaStartedPlaying' ||
eventParams.eventType == 'mediaStoppedPlaying') {
// These events are global to the tree.
eventParams.targetID = privates(targetTree).impl.id;
} else {
var previousFocusedNode = automationUtil.focusedNode;
automationUtil.updateFocusedNode();
// Fire focus events if necessary.
if (automationUtil.focusedNode &&
automationUtil.focusedNode != previousFocusedNode) {
var eventParamsCopy = {};
for (var key in eventParams)
eventParamsCopy[key] = eventParams[key];
eventParamsCopy['eventType'] = 'focus';
eventParamsCopy['treeID'] =
privates(automationUtil.focusedNode.root).impl.treeID;
eventParamsCopy['targetID'] =
privates(automationUtil.focusedNode).impl.id;
privates(automationUtil.focusedNode.root)
.impl.onAccessibilityEvent(eventParamsCopy);
}
}
// Note that focus type events have already been handled above if there was a
// focused node. All other events, even non-focus events that triggered a
// focus dispatch, still need to have their original event fired.
if ((!automationUtil.focusedNode || eventParams.eventType != 'focus') &&
!privates(targetTree).impl.onAccessibilityEvent(eventParams))
return;
// If we're not waiting on a callback to getTree(), we can early out here.
if (!(id in idToCallback))
return;
// We usually get a 'placeholder' tree first, which doesn't have any url
// attribute or child nodes. If we've got that, wait for the full tree before
// calling the callback.
// TODO(dmazzoni): Don't send down placeholder (crbug.com/397553)
if (id != DESKTOP_TREE_ID && !targetTree.url &&
targetTree.children.length == 0)
return;
// If the tree wasn't available when getTree() was called, the callback will
// have been cached in idToCallback, so call and delete it now that we
// have the complete tree.
for (var i = 0; i < idToCallback[id].length; i++) {
var callback = idToCallback[id][i];
callback(targetTree);
}
delete idToCallback[id];
});
automationInternal.onAccessibilityTreeDestroyed.addListener(function(id) {
// Destroy the AutomationRootNode.
var targetTree = AutomationRootNode.get(id);
if (targetTree) {
privates(targetTree).impl.destroy();
AutomationRootNode.destroy(id);
} else {
logging.WARNING('no targetTree to destroy');
}
// Destroy the native cache of the accessibility tree.
DestroyAccessibilityTree(id);
});
automationInternal.onAccessibilityTreeSerializationError.addListener(
function(id) {
automationInternal.enableFrame(id);
});
automationInternal.onActionResult.addListener(
function(treeID, requestID, result) {
var targetTree = AutomationRootNode.get(treeID);
if (!targetTree)
return;
privates(targetTree).impl.onActionResult(requestID, result);
});
if (!apiBridge)
exports.$set('binding', automation.generate());