summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoraboxhall <aboxhall@chromium.org>2014-11-07 19:05:48 -0800
committerCommit bot <commit-bot@chromium.org>2014-11-08 03:06:39 +0000
commit5dcd14b682c37d951f063e2048817ab9afd3f15b (patch)
treefe6deb68943dcc4fecacf6d83610ec70ee7fae1a
parent398e7e1ecb130cc82e16612767b827c57dc4b277 (diff)
downloadchromium_src-5dcd14b682c37d951f063e2048817ab9afd3f15b.zip
chromium_src-5dcd14b682c37d951f063e2048817ab9afd3f15b.tar.gz
chromium_src-5dcd14b682c37d951f063e2048817ab9afd3f15b.tar.bz2
Implement 'find' and 'findAll' in chrome.automation API.
BUG=404710 Review URL: https://codereview.chromium.org/703013002 Cr-Commit-Position: refs/heads/master@{#303369}
-rw-r--r--chrome/browser/extensions/api/automation/automation_apitest.cc6
-rw-r--r--chrome/common/extensions/api/automation.idl35
-rw-r--r--chrome/renderer/resources/extensions/automation/automation_node.js81
-rw-r--r--chrome/test/data/extensions/api_test/automation/tests/tabs/find.html7
-rw-r--r--chrome/test/data/extensions/api_test/automation/tests/tabs/find.js146
5 files changed, 275 insertions, 0 deletions
diff --git a/chrome/browser/extensions/api/automation/automation_apitest.cc b/chrome/browser/extensions/api/automation/automation_apitest.cc
index 8a3dac1..0ac365a 100644
--- a/chrome/browser/extensions/api/automation/automation_apitest.cc
+++ b/chrome/browser/extensions/api/automation/automation_apitest.cc
@@ -180,6 +180,12 @@ IN_PROC_BROWSER_TEST_F(AutomationApiTest, QuerySelector) {
<< message_;
}
+IN_PROC_BROWSER_TEST_F(AutomationApiTest, Find) {
+ StartEmbeddedTestServer();
+ ASSERT_TRUE(RunExtensionSubtest("automation/tests/tabs", "find.html"))
+ << message_;
+}
+
static const int kPid = 1;
static const int kTab0Rid = 1;
static const int kTab1Rid = 2;
diff --git a/chrome/common/extensions/api/automation.idl b/chrome/common/extensions/api/automation.idl
index 8c15eb7..bd7dae4 100644
--- a/chrome/common/extensions/api/automation.idl
+++ b/chrome/common/extensions/api/automation.idl
@@ -219,6 +219,33 @@
long height;
};
+ // Arguments for the find() and findAll() methods.
+ [nocompile, noinline_doc] dictionary FindParams {
+ automation.RoleType? role;
+
+ // A map of $(ref:automation.StateType) to boolean, indicating for each
+ // state whether it should be set or not. For example:
+ // <code>{ StateType.enabled: false }</code> would only match if
+ // <code>StateType.enabled</code> was <em>not</em> present in the node's
+ // <code>state</code> object.
+ object? state;
+
+ // A map of attribute name to expected value, for example
+ // <code>{ name: 'Root directory', button_mixed: true }</code>.
+ // String attribute values may be specified as a regex, for example
+ // <code>{ name: /stralia$/</code> }</code>.
+ // Unless specifying a regex, the expected value must be an exact match
+ // in type and value for the actual value. Thus, the type of expected value
+ // must be one of:
+ // <ul>
+ // <li>string</li>
+ // <li>integer</li>
+ // <li>float</li>
+ // <li>boolean</li>
+ // </ul>
+ object? attributes;
+ };
+
// Called when the result for a <code>query</code> is available.
callback QueryCallback = void(AutomationNode node);
@@ -307,6 +334,14 @@
// aria-hidden), this will return the nearest ancestor which does correspond
// to an automation node.
static void querySelector(DOMString selector, QueryCallback callback);
+
+ // Finds the first AutomationNode in this node's subtree which matches the
+ // given search parameters.
+ static AutomationNode find(FindParams params);
+
+ // Finds all the AutomationNodes in this node's subtree which matches the
+ // given search parameters.
+ static AutomationNode[] findAll(FindParams params);
};
// Called when the <code>AutomationNode</code> for the page is available.
diff --git a/chrome/renderer/resources/extensions/automation/automation_node.js b/chrome/renderer/resources/extensions/automation/automation_node.js
index bc57029..fb34686 100644
--- a/chrome/renderer/resources/extensions/automation/automation_node.js
+++ b/chrome/renderer/resources/extensions/automation/automation_node.js
@@ -101,6 +101,14 @@ AutomationNodeImpl.prototype = {
this.querySelectorCallback_.bind(this, callback));
},
+ find: function(params) {
+ return this.findInternal_(params);
+ },
+
+ findAll: function(params) {
+ return this.findInternal_(params, []);
+ },
+
addEventListener: function(eventType, callback, capture) {
this.removeEventListener(eventType, callback);
if (!this.listeners[eventType])
@@ -241,6 +249,77 @@ AutomationNodeImpl.prototype = {
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;
}
};
@@ -638,6 +717,8 @@ var AutomationNode = utils.expose('AutomationNode',
'previousSibling',
'nextSibling',
'doDefault',
+ 'find',
+ 'findAll',
'focus',
'makeVisible',
'setSelection',
diff --git a/chrome/test/data/extensions/api_test/automation/tests/tabs/find.html b/chrome/test/data/extensions/api_test/automation/tests/tabs/find.html
new file mode 100644
index 0000000..d56ebb0
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/automation/tests/tabs/find.html
@@ -0,0 +1,7 @@
+<!--
+ * 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.
+-->
+<script src="common.js"></script>
+<script src="find.js"></script>
diff --git a/chrome/test/data/extensions/api_test/automation/tests/tabs/find.js b/chrome/test/data/extensions/api_test/automation/tests/tabs/find.js
new file mode 100644
index 0000000..198dfbd
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/automation/tests/tabs/find.js
@@ -0,0 +1,146 @@
+// 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 group;
+var h1;
+var p1;
+var main;
+var p2;
+var p3;
+var anonGroup;
+var okButton;
+var cancelButton;
+
+function initializeNodes(rootNode) {
+ group = rootNode.firstChild();
+ assertEq(RoleType.group, group.role);
+
+ h1 = group.firstChild();
+ assertEq(RoleType.heading, h1.role);
+ assertEq(1, h1.attributes.hierarchicalLevel);
+
+ p1 = group.lastChild();
+ assertEq(RoleType.paragraph, p1.role);
+
+ main = rootNode.children()[1];
+ assertEq(RoleType.main, main.role);
+
+ p2 = main.firstChild();
+ assertEq(RoleType.paragraph, p2.role);
+
+ p3 = main.lastChild();
+ assertEq(RoleType.paragraph, p3.role);
+
+ anonGroup = rootNode.lastChild();
+ assertEq(RoleType.group, anonGroup.role);
+
+ okButton = anonGroup.firstChild();
+ assertEq(RoleType.button, okButton.role);
+ assertEq('Ok', okButton.attributes.name);
+ assertFalse(StateType.enabled in okButton.state);
+
+ cancelButton = anonGroup.lastChild();
+ assertEq(RoleType.button, cancelButton.role);
+ assertEq('Cancel', cancelButton.attributes.name);
+ assertTrue(StateType.enabled in cancelButton.state);
+}
+
+var allTests = [
+ function testFindByRole() {
+ initializeNodes(rootNode);
+
+ // Should find the only instance of this role.
+ assertEq(h1, rootNode.find({ role: RoleType.heading}));
+ assertEq([h1], rootNode.findAll({ role: RoleType.heading}));
+
+ // find should find first instance only.
+ assertEq(okButton, rootNode.find({ role: RoleType.button }));
+ assertEq(p1, rootNode.find({ role: RoleType.paragraph }));
+
+ // findAll should find all instances.
+ assertEq([okButton, cancelButton],
+ rootNode.findAll({ role: RoleType.button }));
+ assertEq([p1, p2, p3], rootNode.findAll({ role: RoleType.paragraph }));
+
+ // No instances: find should return null; findAll should return empty array.
+ assertEq(null, rootNode.find({ role: RoleType.checkbox }));
+ assertEq([], rootNode.findAll({ role: RoleType.checkbox }));
+
+ // Calling from node should search only its subtree.
+ assertEq(p1, group.find({ role: RoleType.paragraph }));
+ assertEq(p2, main.find({ role: RoleType.paragraph }));
+ assertEq([p2, p3], main.findAll({ role: RoleType.paragraph }));
+
+ // Unlike querySelector, can search from an anonymous group without
+ // unexpected results.
+ assertEq(okButton, anonGroup.find({ role: RoleType.button }));
+ assertEq([okButton, cancelButton],
+ anonGroup.findAll({ role: RoleType.button }));
+ assertEq(null, anonGroup.find({ role: RoleType.heading }));
+
+ chrome.test.succeed();
+ },
+
+ function testFindByStates() {
+ initializeNodes(rootNode);
+
+ // Find all focusable elements (disabled button is not focusable).
+ assertEq(cancelButton, rootNode.find({ state: { focusable: true }}));
+ assertEq([cancelButton],
+ rootNode.findAll({ state: { focusable: true }}));
+
+ // Find disabled buttons.
+ assertEq(okButton, rootNode.find({ role: RoleType.button,
+ state: { enabled: false }}));
+ assertEq([okButton], rootNode.findAll({ role: RoleType.button,
+ state: { enabled: false }}));
+
+ // Find disabled buttons within a portion of the tree.
+ assertEq(okButton, anonGroup.find({ role: RoleType.button,
+ state: { enabled: false }}));
+ assertEq([okButton], anonGroup.findAll({ role: RoleType.button,
+ state: { enabled: false }}));
+
+ // Find enabled buttons.
+ assertEq(cancelButton, rootNode.find({ role: RoleType.button,
+ state: { enabled: true }}));
+ assertEq([cancelButton], rootNode.findAll({ role: RoleType.button,
+ state: { enabled: true }}));
+ chrome.test.succeed();
+ },
+
+ function testFindByAttribute() {
+ initializeNodes(rootNode);
+
+ // Find by name attribute.
+ assertEq(okButton, rootNode.find({ attributes: { name: 'Ok' }}));
+ assertEq(cancelButton, rootNode.find({ attributes: { name: 'Cancel' }}));
+
+ // String attributes must be exact match unless a regex is used.
+ assertEq(null, rootNode.find({ attributes: { name: 'Canc' }}));
+ assertEq(null, rootNode.find({ attributes: { name: 'ok' }}));
+
+ // Find by value attribute - regexp.
+ var query = { attributes: { value: /relationship/ }};
+ assertEq(p2, rootNode.find(query).parent());
+
+ // Find by role and hierarchicalLevel attribute.
+ assertEq(h1, rootNode.find({ role: RoleType.heading,
+ attributes: { hierarchicalLevel: 1 }}));
+ assertEq([], rootNode.findAll({ role: RoleType.heading,
+ attributes: { hierarchicalLevel: 2 }}));
+
+ // Searching for an attribute which no element has fails.
+ assertEq(null, rootNode.find({ attributes: { charisma: 12 } }));
+
+ // Searching for an attribute value of the wrong type fails (even if the
+ // value is equivalent).
+ assertEq(null, rootNode.find({ role: RoleType.heading,
+ attributes: { hierarchicalLevel: true }} ));
+
+ chrome.test.succeed();
+ }
+];
+
+setUpAndRunTests(allTests, 'complex.html');