From 5dcd14b682c37d951f063e2048817ab9afd3f15b Mon Sep 17 00:00:00 2001 From: aboxhall Date: Fri, 7 Nov 2014 19:05:48 -0800 Subject: Implement 'find' and 'findAll' in chrome.automation API. BUG=404710 Review URL: https://codereview.chromium.org/703013002 Cr-Commit-Position: refs/heads/master@{#303369} --- .../api/automation/automation_apitest.cc | 6 + chrome/common/extensions/api/automation.idl | 35 +++++ .../extensions/automation/automation_node.js | 81 ++++++++++++ .../api_test/automation/tests/tabs/find.html | 7 + .../api_test/automation/tests/tabs/find.js | 146 +++++++++++++++++++++ 5 files changed, 275 insertions(+) create mode 100644 chrome/test/data/extensions/api_test/automation/tests/tabs/find.html create mode 100644 chrome/test/data/extensions/api_test/automation/tests/tabs/find.js 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: + // { StateType.enabled: false } would only match if + // StateType.enabled was not present in the node's + // state object. + object? state; + + // A map of attribute name to expected value, for example + // { name: 'Root directory', button_mixed: true }. + // String attribute values may be specified as a regex, for example + // { name: /stralia$/ }. + // 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: + // + object? attributes; + }; + // Called when the result for a query 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 AutomationNode 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 @@ + + + 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'); -- cgit v1.1