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
3 // found in the LICENSE file.
5 // Custom bindings for the automation API.
6 var AutomationNode
= require('automationNode').AutomationNode
;
7 var AutomationRootNode
= require('automationNode').AutomationRootNode
;
8 var automation
= require('binding').Binding
.create('automation');
9 var automationInternal
=
10 require('binding').Binding
.create('automationInternal').generate();
11 var eventBindings
= require('event_bindings');
12 var Event
= eventBindings
.Event
;
13 var forEach
= require('utils').forEach
;
14 var lastError
= require('lastError');
15 var logging
= requireNative('logging');
16 var nativeAutomationInternal
= requireNative('automationInternal');
17 var GetRoutingID
= nativeAutomationInternal
.GetRoutingID
;
18 var GetSchemaAdditions
= nativeAutomationInternal
.GetSchemaAdditions
;
19 var DestroyAccessibilityTree
=
20 nativeAutomationInternal
.DestroyAccessibilityTree
;
21 var GetIntAttribute
= nativeAutomationInternal
.GetIntAttribute
;
22 var StartCachingAccessibilityTrees
=
23 nativeAutomationInternal
.StartCachingAccessibilityTrees
;
24 var schema
= GetSchemaAdditions();
27 * A namespace to export utility functions to other files in automation.
29 window
.automationUtil = function() {};
31 // TODO(aboxhall): Look into using WeakMap
32 var idToCallback
= {};
34 var DESKTOP_TREE_ID
= 0;
36 automationUtil
.storeTreeCallback = function(id
, callback
) {
40 var targetTree
= AutomationRootNode
.get(id
);
42 // If we haven't cached the tree, hold the callback until the tree is
43 // populated by the initial onAccessibilityEvent call.
44 if (id
in idToCallback
)
45 idToCallback
[id
].push(callback
);
47 idToCallback
[id
] = [callback
];
54 * Global list of tree change observers.
55 * @type {Array<TreeChangeObserver>}
57 automationUtil
.treeChangeObservers
= [];
59 automation
.registerCustomHook(function(bindingsAPI
) {
60 var apiFunctions
= bindingsAPI
.apiFunctions
;
62 // TODO(aboxhall, dtseng): Make this return the speced AutomationRootNode obj.
63 apiFunctions
.setHandleRequest('getTree', function getTree(tabID
, callback
) {
64 var routingID
= GetRoutingID();
65 StartCachingAccessibilityTrees();
67 // enableTab() ensures the renderer for the active or specified tab has
68 // accessibility enabled, and fetches its ax tree id to use as
69 // a key in the idToAutomationRootNode map. The callback to
70 // enableTab is bound to the callback passed in to getTree(), so that once
71 // the tree is available (either due to having been cached earlier, or after
72 // an accessibility event occurs which causes the tree to be populated), the
73 // callback can be called.
74 var params
= { routingID
: routingID
, tabID
: tabID
};
75 automationInternal
.enableTab(params
,
76 function onEnable(id
) {
77 if (lastError
.hasError(chrome
)) {
81 automationUtil
.storeTreeCallback(id
, callback
);
85 var desktopTree
= null;
86 apiFunctions
.setHandleRequest('getDesktop', function(callback
) {
87 StartCachingAccessibilityTrees();
88 desktopTree
= AutomationRootNode
.get(DESKTOP_TREE_ID
);
90 if (DESKTOP_TREE_ID
in idToCallback
)
91 idToCallback
[DESKTOP_TREE_ID
].push(callback
);
93 idToCallback
[DESKTOP_TREE_ID
] = [callback
];
95 var routingID
= GetRoutingID();
97 // TODO(dtseng): Disable desktop tree once desktop object goes out of
99 automationInternal
.enableDesktop(routingID
, function() {
100 if (lastError
.hasError(chrome
)) {
101 AutomationRootNode
.destroy(DESKTOP_TREE_ID
);
107 callback(desktopTree
);
111 function removeTreeChangeObserver(observer
) {
112 var observers
= automationUtil
.treeChangeObservers
;
113 for (var i
= 0; i
< observers
.length
; i
++) {
114 if (observer
== observers
[i
])
115 observers
.splice(i
, 1);
118 apiFunctions
.setHandleRequest('removeTreeChangeObserver', function(observer
) {
119 removeTreeChangeObserver(observer
);
122 function addTreeChangeObserver(observer
) {
123 removeTreeChangeObserver(observer
);
124 automationUtil
.treeChangeObservers
.push(observer
);
126 apiFunctions
.setHandleRequest('addTreeChangeObserver', function(observer
) {
127 addTreeChangeObserver(observer
);
132 automationInternal
.onTreeChange
.addListener(function(treeID
,
135 var tree
= AutomationRootNode
.getOrCreate(treeID
);
139 var node
= privates(tree
).impl
.get(nodeID
);
143 if (node
.role
== 'webView' || node
.role
== 'embeddedObject') {
144 // A WebView in the desktop tree has a different AX tree as its child.
145 // When we encounter a WebView with a child AX tree id that we don't
146 // currently have cached, explicitly request that AX tree from the
147 // browser process and set up a callback when it loads to attach that
148 // tree as a child of this node and fire appropriate events.
149 var childTreeID
= GetIntAttribute(treeID
, nodeID
, 'childTreeId');
153 var subroot
= AutomationRootNode
.get(childTreeID
);
155 automationUtil
.storeTreeCallback(childTreeID
, function(root
) {
156 privates(root
).impl
.setHostNode(node
);
159 privates(root
).impl
.dispatchEvent(schema
.EventType
.loadComplete
);
161 privates(node
).impl
.dispatchEvent(schema
.EventType
.childrenChanged
);
164 automationInternal
.enableFrame(childTreeID
);
166 privates(subroot
).impl
.setHostNode(node
);
170 var treeChange
= {target
: node
, type
: changeType
};
172 // Make a copy of the observers in case one of these callbacks tries
173 // to change the list of observers.
174 var observers
= automationUtil
.treeChangeObservers
.slice();
175 for (var i
= 0; i
< observers
.length
; i
++) {
177 observers
[i
](treeChange
);
179 logging
.WARNING('Error in tree change observer for ' +
180 treeChange
.type
+ ': ' + e
.message
+
181 '\nStack trace: ' + e
.stack
);
185 if (changeType
== schema
.TreeChangeType
.nodeRemoved
) {
186 privates(tree
).impl
.remove(nodeID
);
190 // Listen to the automationInternal.onAccessibilityEvent event, which is
191 // essentially a proxy for the AccessibilityHostMsg_Events IPC from the
193 automationInternal
.onAccessibilityEvent
.addListener(function(data
) {
194 var id
= data
.treeID
;
195 var targetTree
= AutomationRootNode
.getOrCreate(id
);
197 if (!privates(targetTree
).impl
.onAccessibilityEvent(data
))
200 // If we're not waiting on a callback to getTree(), we can early out here.
201 if (!(id
in idToCallback
))
204 // We usually get a 'placeholder' tree first, which doesn't have any url
205 // attribute or child nodes. If we've got that, wait for the full tree before
206 // calling the callback.
207 // TODO(dmazzoni): Don't send down placeholder (crbug.com/397553)
208 if (id
!= DESKTOP_TREE_ID
&& !targetTree
.url
&&
209 targetTree
.children
.length
== 0)
212 // If the tree wasn't available when getTree() was called, the callback will
213 // have been cached in idToCallback, so call and delete it now that we
214 // have the complete tree.
215 for (var i
= 0; i
< idToCallback
[id
].length
; i
++) {
216 var callback
= idToCallback
[id
][i
];
217 callback(targetTree
);
219 delete idToCallback
[id
];
222 automationInternal
.onAccessibilityTreeDestroyed
.addListener(function(id
) {
223 // Destroy the AutomationRootNode.
224 var targetTree
= AutomationRootNode
.get(id
);
226 privates(targetTree
).impl
.destroy();
227 AutomationRootNode
.destroy(id
);
229 logging
.WARNING('no targetTree to destroy');
232 // Destroy the native cache of the accessibility tree.
233 DestroyAccessibilityTree(id
);
236 exports
.binding
= automation
.generate();
238 // Add additional accessibility bindings not specified in the automation IDL.
239 // Accessibility and automation share some APIs (see
240 // ui/accessibility/ax_enums.idl).
241 forEach(schema
, function(k
, v
) {
242 exports
.binding
[k
] = v
;