diff options
author | dmazzoni <dmazzoni@chromium.org> | 2015-07-22 12:17:10 -0700 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2015-07-22 19:18:36 +0000 |
commit | 1777fbdbd234ddc8e19d4ba3a42cfd3234b8a158 (patch) | |
tree | 76e29ea1fd30a3c40a577685c4bf7e19eb632ebe | |
parent | bb2780cad4e11b3fdde2690428e90be003f5be73 (diff) | |
download | chromium_src-1777fbdbd234ddc8e19d4ba3a42cfd3234b8a158.zip chromium_src-1777fbdbd234ddc8e19d4ba3a42cfd3234b8a158.tar.gz chromium_src-1777fbdbd234ddc8e19d4ba3a42cfd3234b8a158.tar.bz2 |
Re-land: Reimplement automation API on top of C++-backed AXTree.
Original review: https://codereview.chromium.org/1155183006
Landed in: r335183
Reverted in: r335343 (bug 502311)
BUG=495323,502311
Review URL: https://codereview.chromium.org/1231603009
Cr-Commit-Position: refs/heads/master@{#339929}
29 files changed, 1550 insertions, 2154 deletions
diff --git a/chrome/browser/extensions/api/automation/automation_apitest.cc b/chrome/browser/extensions/api/automation/automation_apitest.cc index 8505161..5f1adcf 100644 --- a/chrome/browser/extensions/api/automation/automation_apitest.cc +++ b/chrome/browser/extensions/api/automation/automation_apitest.cc @@ -8,13 +8,15 @@ #include "base/single_thread_task_runner.h" #include "base/strings/string_number_conversions.h" #include "base/thread_task_runner_handle.h" -#include "chrome/browser/extensions/api/automation_internal/automation_util.h" +#include "chrome/browser/accessibility/ax_tree_id_registry.h" +#include "chrome/browser/extensions/api/automation_internal/automation_event_router.h" #include "chrome/browser/extensions/chrome_extension_function.h" #include "chrome/browser/extensions/extension_apitest.h" #include "chrome/browser/ui/tabs/tab_strip_model.h" #include "chrome/common/chrome_paths.h" #include "chrome/common/chrome_switches.h" #include "chrome/common/extensions/api/automation_internal.h" +#include "chrome/common/extensions/chrome_extension_messages.h" #include "chrome/test/base/ui_test_utils.h" #include "content/public/browser/ax_event_notification_details.h" #include "content/public/browser/render_widget_host.h" @@ -99,11 +101,6 @@ IN_PROC_BROWSER_TEST_F(AutomationApiTest, SanityCheck) { << message_; } -IN_PROC_BROWSER_TEST_F(AutomationApiTest, Unit) { - ASSERT_TRUE(RunExtensionSubtest("automation/tests/unit", "unit.html")) - << message_; -} - IN_PROC_BROWSER_TEST_F(AutomationApiTest, GetTreeByTabId) { StartEmbeddedTestServer(); ASSERT_TRUE(RunExtensionSubtest("automation/tests/tabs", "tab_id.html")) @@ -203,10 +200,9 @@ IN_PROC_BROWSER_TEST_F(AutomationApiTest, Find) { << message_; } -// Flaky. http://crbug.com/467921 -IN_PROC_BROWSER_TEST_F(AutomationApiTest, DISABLED_Mixins) { +IN_PROC_BROWSER_TEST_F(AutomationApiTest, Attributes) { StartEmbeddedTestServer(); - ASSERT_TRUE(RunExtensionSubtest("automation/tests/tabs", "mixins.html")) + ASSERT_TRUE(RunExtensionSubtest("automation/tests/tabs", "attributes.html")) << message_; } @@ -216,328 +212,4 @@ IN_PROC_BROWSER_TEST_F(AutomationApiTest, TreeChange) { << message_; } - -static const int kPid = 1; -static const int kTab0Rid = 1; -static const int kTab1Rid = 2; - -using content::BrowserContext; - -typedef ui::AXTreeSerializer<const ui::AXNode*> TreeSerializer; -typedef ui::AXTreeSource<const ui::AXNode*> TreeSource; - -#define AX_EVENT_ASSERT_EQUAL ui::AX_EVENT_LOAD_COMPLETE -#define AX_EVENT_ASSERT_NOT_EQUAL ui::AX_EVENT_ACTIVEDESCENDANTCHANGED -#define AX_EVENT_IGNORE ui::AX_EVENT_CHILDREN_CHANGED -#define AX_EVENT_TEST_COMPLETE ui::AX_EVENT_BLUR - -// This test is based on ui/accessibility/ax_generated_tree_unittest.cc -// However, because the tree updates need to be sent to the extension, we can't -// use a straightforward set of nested loops as that test does, so this class -// keeps track of where we're up to in our imaginary loops, while the extension -// function classes below do the work of actually incrementing the state when -// appropriate. -// The actual deserialization and comparison happens in the API bindings and the -// test extension respectively: see -// c/t/data/extensions/api_test/automation/tests/generated/generated_trees.js -class TreeSerializationState { - public: - TreeSerializationState() -#ifdef NDEBUG - : tree_size(3), -#else - : tree_size(2), -#endif - generator(tree_size, true), - num_trees(generator.UniqueTreeCount()), - tree0_version(0), - tree1_version(0) { - } - - // Serializes tree and sends it as an accessibility event to the extension. - void SendDataForTree(const ui::AXTree* tree, - TreeSerializer* serializer, - int routing_id, - BrowserContext* browser_context) { - ui::AXTreeUpdate update; - serializer->SerializeChanges(tree->root(), &update); - SendUpdate(update, - ui::AX_EVENT_LAYOUT_COMPLETE, - tree->root()->id(), - routing_id, - browser_context); - } - - // Sends the given AXTreeUpdate to the extension as an accessibility event. - void SendUpdate(ui::AXTreeUpdate update, - ui::AXEvent event, - int node_id, - int routing_id, - BrowserContext* browser_context) { - content::AXEventNotificationDetails detail(update.node_id_to_clear, - update.nodes, - event, - node_id, - std::map<int32, int>(), - kPid, - routing_id); - std::vector<content::AXEventNotificationDetails> details; - details.push_back(detail); - automation_util::DispatchAccessibilityEventsToAutomation( - details, browser_context, gfx::Vector2d()); - } - - // Notify the extension bindings to destroy the tree for the given tab - // (identified by routing_id) - void SendTreeDestroyedEvent(int routing_id, BrowserContext* browser_context) { - automation_util::DispatchTreeDestroyedEventToAutomation( - kPid, routing_id, browser_context); - } - - // Reset tree0 to a new generated tree based on tree0_version, reset - // tree0_source accordingly. - void ResetTree0() { - tree0.reset(new ui::AXSerializableTree); - tree0_source.reset(tree0->CreateTreeSource()); - generator.BuildUniqueTree(tree0_version, tree0.get()); - if (!serializer0.get()) - serializer0.reset(new TreeSerializer(tree0_source.get())); - } - - // Reset tree0, set up serializer0, send down the initial tree data to create - // the tree in the extension - void InitializeTree0(BrowserContext* browser_context) { - ResetTree0(); - serializer0->ChangeTreeSourceForTesting(tree0_source.get()); - serializer0->Reset(); - SendDataForTree(tree0.get(), serializer0.get(), kTab0Rid, browser_context); - } - - // Reset tree1 to a new generated tree based on tree1_version, reset - // tree1_source accordingly. - void ResetTree1() { - tree1.reset(new ui::AXSerializableTree); - tree1_source.reset(tree1->CreateTreeSource()); - generator.BuildUniqueTree(tree1_version, tree1.get()); - if (!serializer1.get()) - serializer1.reset(new TreeSerializer(tree1_source.get())); - } - - // Reset tree1, set up serializer1, send down the initial tree data to create - // the tree in the extension - void InitializeTree1(BrowserContext* browser_context) { - ResetTree1(); - serializer1->ChangeTreeSourceForTesting(tree1_source.get()); - serializer1->Reset(); - SendDataForTree(tree1.get(), serializer1.get(), kTab1Rid, browser_context); - } - - const int tree_size; - const ui::TreeGenerator generator; - - // The loop variables: comments indicate which variables in - // ax_generated_tree_unittest they correspond to. - const int num_trees; // n - int tree0_version; // i - int tree1_version; // j - int starting_node; // k - - // Tree infrastructure; tree0 and tree1 need to be regenerated whenever - // tree0_version and tree1_version change, respectively; tree0_source and - // tree1_source need to be reset whenever that happens. - scoped_ptr<ui::AXSerializableTree> tree0, tree1; - scoped_ptr<TreeSource> tree0_source, tree1_source; - scoped_ptr<TreeSerializer> serializer0, serializer1; - - // Whether tree0 needs to be destroyed after the extension has performed its - // checks - bool destroy_tree0; -}; - -static TreeSerializationState state; - -// Override for chrome.automationInternal.enableTab -// This fakes out the process and routing IDs for two "tabs", which contain the -// source and target trees, respectively, and sends down the current tree for -// the requested tab - tab 1 always has tree1, and tab 0 starts with tree0 -// and then has a series of updates intended to translate tree0 to tree1. -// Once all the updates have been sent, the extension asserts that both trees -// are equivalent, and then one or both of the trees are reset to a new version. -class FakeAutomationInternalEnableTabFunction - : public UIThreadExtensionFunction { - public: - FakeAutomationInternalEnableTabFunction() {} - - ExtensionFunction::ResponseAction Run() override { - using api::automation_internal::EnableTab::Params; - scoped_ptr<Params> params(Params::Create(*args_)); - EXTENSION_FUNCTION_VALIDATE(params.get()); - if (!params->args.tab_id.get()) - return RespondNow(Error("tab_id not specified")); - int tab_id = *params->args.tab_id; - if (tab_id == 0) { - // tab 0 <--> tree0 - base::ThreadTaskRunnerHandle::Get()->PostTask( - FROM_HERE, base::Bind(&TreeSerializationState::InitializeTree0, - base::Unretained(&state), - base::Unretained(browser_context()))); - // TODO(aboxhall): Need to rewrite this test in terms of tree ids. - return RespondNow(ArgumentList( - api::automation_internal::EnableTab::Results::Create(0))); - } - if (tab_id == 1) { - // tab 1 <--> tree1 - base::ThreadTaskRunnerHandle::Get()->PostTask( - FROM_HERE, base::Bind(&TreeSerializationState::InitializeTree1, - base::Unretained(&state), - base::Unretained(browser_context()))); - return RespondNow(ArgumentList( - api::automation_internal::EnableTab::Results::Create(0))); - } - return RespondNow(Error("Unrecognised tab_id")); - } -}; - -// Factory method for use in OverrideFunction() -ExtensionFunction* FakeAutomationInternalEnableTabFunctionFactory() { - return new FakeAutomationInternalEnableTabFunction(); -} - -// Helper method to serialize a series of updates via source_serializer to -// transform the tree which source_serializer was initialized from into -// target_tree, and then trigger the test code to assert the two tabs contain -// the same tree. -void TransformTree(TreeSerializer* source_serializer, - ui::AXTree* target_tree, - TreeSource* target_tree_source, - content::BrowserContext* browser_context) { - source_serializer->ChangeTreeSourceForTesting(target_tree_source); - for (int node_delta = 0; node_delta < state.tree_size; ++node_delta) { - int id = 1 + (state.starting_node + node_delta) % state.tree_size; - ui::AXTreeUpdate update; - source_serializer->SerializeChanges(target_tree->GetFromId(id), &update); - bool is_last_update = node_delta == state.tree_size - 1; - ui::AXEvent event = - is_last_update ? AX_EVENT_ASSERT_EQUAL : AX_EVENT_IGNORE; - state.SendUpdate( - update, event, target_tree->root()->id(), kTab0Rid, browser_context); - } -} - -// Helper method to send a no-op tree update to tab 0 with the given event. -void SendEvent(ui::AXEvent event, content::BrowserContext* browser_context) { - ui::AXTreeUpdate update; - ui::AXNode* root = state.tree0->root(); - state.serializer0->SerializeChanges(root, &update); - state.SendUpdate(update, event, root->id(), kTab0Rid, browser_context); -} - -// Override for chrome.automationInternal.performAction -// This is used as a synchronization mechanism; the general flow is: -// 1. The extension requests tree0 and tree1 (on tab 0 and tab 1 respectively) -// 2. FakeAutomationInternalEnableTabFunction sends down the trees -// 3. When the callback for getTree(0) fires, the extension calls doDefault() on -// the root node of tree0, which calls into this class's Run() method. -// 4. In the normal case, we're in the "inner loop" (iterating over -// starting_node). For each value of starting_node, we do the following: -// a. Serialize a sequence of updates which should transform tree0 into -// tree1. Each of these updates is sent as a childrenChanged event, -// except for the last which is sent as a loadComplete event. -// b. state.destroy_tree0 is set to true -// c. state.starting_node gets incremented -// d. The loadComplete event triggers an assertion in the extension. -// e. The extension performs another doDefault() on the root node of the -// tree. -// f. This time, we send a destroy event to tab0, so that the tree can be -// reset. -// g. The extension is notified of the tree's destruction and requests the -// tree for tab 0 again, returning to step 2. -// 5. When starting_node exceeds state.tree_size, we increment tree0_version if -// it would not exceed state.num_trees, or increment tree1_version and reset -// tree0_version to 0 otherwise, and reset starting_node to 0. -// Then we reset one or both trees as appropriate, and send down destroyed -// events similarly, causing the extension to re-request the tree and going -// back to step 2 again. -// 6. When tree1_version has gone through all possible values, we send a blur -// event, signaling the extension to call chrome.test.succeed() and finish -// the test. -class FakeAutomationInternalPerformActionFunction - : public UIThreadExtensionFunction { - public: - FakeAutomationInternalPerformActionFunction() {} - - ExtensionFunction::ResponseAction Run() override { - if (state.destroy_tree0) { - // Step 4.f: tell the extension to destroy the tree and re-request it. - state.SendTreeDestroyedEvent(kTab0Rid, browser_context()); - state.destroy_tree0 = false; - return RespondNow(NoArguments()); - } - - TreeSerializer* serializer0 = state.serializer0.get(); - if (state.starting_node < state.tree_size) { - // As a sanity check, if the trees are not equal, assert that they are not - // equal before serializing changes. - if (state.tree0_version != state.tree1_version) - SendEvent(AX_EVENT_ASSERT_NOT_EQUAL, browser_context()); - - // Step 4.a: pretend that tree0 turned into tree1, and serialize - // a sequence of updates to tab 0 to match. - TransformTree(serializer0, - state.tree1.get(), - state.tree1_source.get(), - browser_context()); - - // Step 4.b: remember that we need to tell the extension to destroy and - // re-request the tree on the next action. - state.destroy_tree0 = true; - - // Step 4.c: increment starting_node. - state.starting_node++; - } else if (state.tree0_version < state.num_trees - 1) { - // Step 5: Increment tree0_version and reset starting_node - state.tree0_version++; - state.starting_node = 0; - - // Step 5: Reset tree0 and tell the extension to destroy and re-request it - state.SendTreeDestroyedEvent(kTab0Rid, browser_context()); - } else if (state.tree1_version < state.num_trees - 1) { - // Step 5: Increment tree1_version and reset tree0_version and - // starting_node - state.tree1_version++; - state.tree0_version = 0; - state.starting_node = 0; - - // Step 5: Reset tree0 and tell the extension to destroy and re-request it - state.SendTreeDestroyedEvent(kTab0Rid, browser_context()); - - // Step 5: Reset tree1 and tell the extension to destroy and re-request it - state.SendTreeDestroyedEvent(kTab1Rid, browser_context()); - } else { - // Step 6: Send a TEST_COMPLETE (blur) event to signal the extension to - // call chrome.test.succeed(). - SendEvent(AX_EVENT_TEST_COMPLETE, browser_context()); - } - - return RespondNow(NoArguments()); - } -}; - -// Factory method for use in OverrideFunction() -ExtensionFunction* FakeAutomationInternalPerformActionFunctionFactory() { - return new FakeAutomationInternalPerformActionFunction(); -} - -// http://crbug.com/396353 -IN_PROC_BROWSER_TEST_F(AutomationApiTest, DISABLED_GeneratedTree) { - ASSERT_TRUE(extensions::ExtensionFunctionDispatcher::OverrideFunction( - "automationInternal.enableTab", - FakeAutomationInternalEnableTabFunctionFactory)); - ASSERT_TRUE(extensions::ExtensionFunctionDispatcher::OverrideFunction( - "automationInternal.performAction", - FakeAutomationInternalPerformActionFunctionFactory)); - ASSERT_TRUE(RunExtensionSubtest("automation/tests/generated", - "generated_trees.html")) << message_; -} - } // namespace extensions diff --git a/chrome/browser/extensions/api/automation_internal/automation_event_router.cc b/chrome/browser/extensions/api/automation_internal/automation_event_router.cc index c62a3fa..89ae0c7 100644 --- a/chrome/browser/extensions/api/automation_internal/automation_event_router.cc +++ b/chrome/browser/extensions/api/automation_internal/automation_event_router.cc @@ -62,7 +62,8 @@ void AutomationEventRouter::DispatchAccessibilityEvent( content::RenderProcessHost* rph = content::RenderProcessHost::FromID(listener.process_id); - rph->Send(new ExtensionMsg_AccessibilityEvent(listener.routing_id, params)); + rph->Send(new ExtensionMsg_AccessibilityEvent(listener.routing_id, + params)); } } @@ -93,7 +94,8 @@ void AutomationEventRouter::Register( auto iter = std::find_if( listeners_.begin(), listeners_.end(), - [listener_process_id, listener_routing_id](AutomationListener& item) { + [listener_process_id, listener_routing_id]( + const AutomationListener& item) { return (item.process_id == listener_process_id && item.routing_id == listener_routing_id); }); @@ -132,8 +134,8 @@ void AutomationEventRouter::Observe( std::remove_if( listeners_.begin(), listeners_.end(), - [process_id](AutomationListener& item) { - return item.process_id = process_id; + [process_id](const AutomationListener& item) { + return item.process_id == process_id; }); } diff --git a/chrome/browser/extensions/api/automation_internal/automation_internal_api.cc b/chrome/browser/extensions/api/automation_internal/automation_internal_api.cc index 5f2e8c8..58efba5 100644 --- a/chrome/browser/extensions/api/automation_internal/automation_internal_api.cc +++ b/chrome/browser/extensions/api/automation_internal/automation_internal_api.cc @@ -12,7 +12,6 @@ #include "chrome/browser/accessibility/ax_tree_id_registry.h" #include "chrome/browser/extensions/api/automation_internal/automation_action_adapter.h" #include "chrome/browser/extensions/api/automation_internal/automation_event_router.h" -#include "chrome/browser/extensions/api/automation_internal/automation_util.h" #include "chrome/browser/extensions/api/tabs/tabs_constants.h" #include "chrome/browser/extensions/extension_tab_util.h" #include "chrome/browser/profiles/profile.h" @@ -23,12 +22,16 @@ #include "chrome/common/extensions/manifest_handlers/automation.h" #include "content/public/browser/ax_event_notification_details.h" #include "content/public/browser/browser_accessibility_state.h" +#include "content/public/browser/browser_context.h" +#include "content/public/browser/browser_plugin_guest_manager.h" #include "content/public/browser/render_frame_host.h" #include "content/public/browser/render_process_host.h" #include "content/public/browser/render_view_host.h" #include "content/public/browser/render_widget_host.h" #include "content/public/browser/render_widget_host_view.h" #include "content/public/browser/web_contents.h" +#include "content/public/browser/web_contents_observer.h" +#include "content/public/browser/web_contents_user_data.h" #include "extensions/common/extension_messages.h" #include "extensions/common/permissions/permissions_data.h" @@ -45,6 +48,7 @@ DEFINE_WEB_CONTENTS_USER_DATA_KEY(extensions::AutomationWebContentsObserver); namespace extensions { namespace { + const int kDesktopTreeID = 0; const char kCannotRequestAutomationOnPage[] = "Cannot request automation tree on url \"*\". " @@ -193,24 +197,66 @@ class AutomationWebContentsObserver void AccessibilityEventReceived( const std::vector<content::AXEventNotificationDetails>& details) override { - automation_util::DispatchAccessibilityEventsToAutomation( - details, browser_context_, - web_contents()->GetContainerBounds().OffsetFromOrigin()); + std::vector<content::AXEventNotificationDetails>::const_iterator iter = + details.begin(); + for (; iter != details.end(); ++iter) { + const content::AXEventNotificationDetails& event = *iter; + int tree_id = AXTreeIDRegistry::GetInstance()->GetOrCreateAXTreeID( + event.process_id, event.routing_id); + ExtensionMsg_AccessibilityEventParams params; + params.tree_id = tree_id; + params.id = event.id; + params.event_type = event.event_type; + params.update.node_id_to_clear = event.node_id_to_clear; + params.update.nodes = event.nodes; + params.location_offset = + web_contents()->GetContainerBounds().OffsetFromOrigin(); + + for (size_t i = 0; i < params.update.nodes.size(); ++i) { + ui::AXNodeData& node = params.update.nodes[i]; + if (node.HasBoolAttribute(ui::AX_ATTR_IS_AX_TREE_HOST)) { + const auto& iter = event.node_to_browser_plugin_instance_id_map.find( + node.id); + if (iter != event.node_to_browser_plugin_instance_id_map.end()) { + int instance_id = iter->second; + content::BrowserPluginGuestManager* guest_manager = + browser_context_->GetGuestManager(); + content::WebContents* guest_web_contents = + guest_manager->GetGuestByInstanceID(event.process_id, + instance_id); + if (guest_web_contents) { + content::RenderFrameHost* guest_rfh = + guest_web_contents->GetMainFrame(); + int guest_tree_id = + AXTreeIDRegistry::GetInstance()->GetOrCreateAXTreeID( + guest_rfh->GetProcess()->GetID(), + guest_rfh->GetRoutingID()); + node.AddIntAttribute(ui::AX_ATTR_CHILD_TREE_ID, guest_tree_id); + } + } + } + } + + AutomationEventRouter* router = AutomationEventRouter::GetInstance(); + router->DispatchAccessibilityEvent(params); + } } void RenderFrameDeleted( content::RenderFrameHost* render_frame_host) override { - automation_util::DispatchTreeDestroyedEventToAutomation( + int tree_id = AXTreeIDRegistry::GetInstance()->GetOrCreateAXTreeID( render_frame_host->GetProcess()->GetID(), - render_frame_host->GetRoutingID(), + render_frame_host->GetRoutingID()); + AXTreeIDRegistry::GetInstance()->RemoveAXTreeID(tree_id); + AutomationEventRouter::GetInstance()->DispatchTreeDestroyedEvent( + tree_id, browser_context_); } private: friend class content::WebContentsUserData<AutomationWebContentsObserver>; - AutomationWebContentsObserver( - content::WebContents* web_contents) + explicit AutomationWebContentsObserver(content::WebContents* web_contents) : content::WebContentsObserver(web_contents), browser_context_(web_contents->GetBrowserContext()) {} @@ -245,6 +291,7 @@ AutomationInternalEnableTabFunction::Run() { if (!contents) return RespondNow(Error("No active tab")); } + content::RenderFrameHost* rfh = contents->GetMainFrame(); if (!rfh) return RespondNow(Error("Could not enable accessibility for active tab")); @@ -256,6 +303,7 @@ AutomationInternalEnableTabFunction::Run() { AutomationWebContentsObserver::CreateForWebContents(contents); contents->EnableTreeOnlyAccessibilityMode(); + int ax_tree_id = AXTreeIDRegistry::GetInstance()->GetOrCreateAXTreeID( rfh->GetProcess()->GetID(), rfh->GetRoutingID()); @@ -270,11 +318,12 @@ AutomationInternalEnableTabFunction::Run() { } ExtensionFunction::ResponseAction AutomationInternalEnableFrameFunction::Run() { -// TODO(dtseng): Limited to desktop tree for now pending out of proc iframes. + // TODO(dtseng): Limited to desktop tree for now pending out of proc iframes. using api::automation_internal::EnableFrame::Params; scoped_ptr<Params> params(Params::Create(*args_)); EXTENSION_FUNCTION_VALIDATE(params.get()); + AXTreeIDRegistry::FrameID frame_id = AXTreeIDRegistry::GetInstance()->GetFrameID(params->tree_id); content::RenderFrameHost* rfh = diff --git a/chrome/browser/extensions/api/automation_internal/automation_util.cc b/chrome/browser/extensions/api/automation_internal/automation_util.cc deleted file mode 100644 index a16654b..0000000 --- a/chrome/browser/extensions/api/automation_internal/automation_util.cc +++ /dev/null @@ -1,215 +0,0 @@ -// 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. - -#include "chrome/browser/extensions/api/automation_internal/automation_util.h" - -#include <string> -#include <utility> - -#include "base/values.h" -#include "chrome/browser/accessibility/ax_tree_id_registry.h" -#include "chrome/common/extensions/api/automation_internal.h" -#include "content/public/browser/browser_context.h" -#include "content/public/browser/browser_plugin_guest_manager.h" -#include "content/public/browser/render_frame_host.h" -#include "content/public/browser/render_process_host.h" -#include "content/public/browser/web_contents.h" -#include "extensions/browser/event_router.h" -#include "ui/accessibility/ax_enums.h" -#include "ui/accessibility/ax_node_data.h" - -namespace extensions { - -namespace { - -void PopulateNodeData(const ui::AXNodeData& node_data, - linked_ptr< api::automation_internal::AXNodeData>& out_node_data) { - out_node_data->id = node_data.id; - out_node_data->role = ToString(node_data.role); - - uint32 state_pos = 0, state_shifter = node_data.state; - while (state_shifter) { - if (state_shifter & 1) { - out_node_data->state.additional_properties.SetBoolean( - ToString(static_cast<ui::AXState>(state_pos)), true); - } - state_shifter = state_shifter >> 1; - state_pos++; - } - - out_node_data->location.left = node_data.location.x(); - out_node_data->location.top = node_data.location.y(); - out_node_data->location.width = node_data.location.width(); - out_node_data->location.height = node_data.location.height(); - - if (!node_data.bool_attributes.empty()) { - out_node_data->bool_attributes.reset( - new api::automation_internal::AXNodeData::BoolAttributes()); - for (size_t i = 0; i < node_data.bool_attributes.size(); ++i) { - std::pair<ui::AXBoolAttribute, bool> attr = - node_data.bool_attributes[i]; - out_node_data->bool_attributes->additional_properties.SetBoolean( - ToString(attr.first), attr.second); - } - } - - if (!node_data.float_attributes.empty()) { - out_node_data->float_attributes.reset( - new api::automation_internal::AXNodeData::FloatAttributes()); - for (size_t i = 0; i < node_data.float_attributes.size(); ++i) { - std::pair<ui::AXFloatAttribute, float> attr = - node_data.float_attributes[i]; - out_node_data->float_attributes->additional_properties.SetDouble( - ToString(attr.first), attr.second); - } - } - - if (!node_data.html_attributes.empty()) { - out_node_data->html_attributes.reset( - new api::automation_internal::AXNodeData::HtmlAttributes()); - for (size_t i = 0; i < node_data.html_attributes.size(); ++i) { - std::pair<std::string, std::string> attr = node_data.html_attributes[i]; - out_node_data->html_attributes->additional_properties.SetString( - attr.first, attr.second); - } - } - - if (!node_data.int_attributes.empty()) { - out_node_data->int_attributes.reset( - new api::automation_internal::AXNodeData::IntAttributes()); - for (size_t i = 0; i < node_data.int_attributes.size(); ++i) { - std::pair<ui::AXIntAttribute, int> attr = node_data.int_attributes[i]; - out_node_data->int_attributes->additional_properties.SetInteger( - ToString(attr.first), attr.second); - } - } - - if (!node_data.intlist_attributes.empty()) { - out_node_data->intlist_attributes.reset( - new api::automation_internal::AXNodeData::IntlistAttributes()); - for (size_t i = 0; i < node_data.intlist_attributes.size(); ++i) { - std::pair<ui::AXIntListAttribute, std::vector<int32> > attr = - node_data.intlist_attributes[i]; - base::ListValue* intlist = new base::ListValue(); - for (size_t j = 0; j < attr.second.size(); ++j) - intlist->AppendInteger(attr.second[j]); - out_node_data->intlist_attributes->additional_properties.Set( - ToString(attr.first), intlist); - } - } - - if (!node_data.string_attributes.empty()) { - out_node_data->string_attributes.reset( - new api::automation_internal::AXNodeData::StringAttributes()); - for (size_t i = 0; i < node_data.string_attributes.size(); ++i) { - std::pair<ui::AXStringAttribute, std::string> attr = - node_data.string_attributes[i]; - out_node_data->string_attributes->additional_properties.SetString( - ToString(attr.first), attr.second); - } - } - - for (size_t i = 0; i < node_data.child_ids.size(); ++i) { - out_node_data->child_ids.push_back(node_data.child_ids[i]); - } -} - -void DispatchEventInternal(content::BrowserContext* context, - events::HistogramValue histogram_value, - const std::string& event_name, - scoped_ptr<base::ListValue> args) { - if (context && EventRouter::Get(context)) { - scoped_ptr<Event> event( - new Event(histogram_value, event_name, args.Pass())); - event->restrict_to_browser_context = context; - EventRouter::Get(context)->BroadcastEvent(event.Pass()); - } -} - -} // namespace - -namespace automation_util { - -void DispatchAccessibilityEventsToAutomation( - const std::vector<content::AXEventNotificationDetails>& details, - content::BrowserContext* browser_context, - const gfx::Vector2d& location_offset) { - using api::automation_internal::AXEventParams; - using api::automation_internal::AXTreeUpdate; - - std::vector<content::AXEventNotificationDetails>::const_iterator iter = - details.begin(); - for (; iter != details.end(); ++iter) { - const content::AXEventNotificationDetails& event = *iter; - - AXEventParams ax_event_params; - ax_event_params.tree_id = - AXTreeIDRegistry::GetInstance()->GetOrCreateAXTreeID(event.process_id, - event.routing_id); - ax_event_params.event_type = ToString(iter->event_type); - ax_event_params.target_id = event.id; - - AXTreeUpdate& ax_tree_update = ax_event_params.update; - ax_tree_update.node_id_to_clear = event.node_id_to_clear; - for (size_t i = 0; i < event.nodes.size(); ++i) { - ui::AXNodeData src = event.nodes[i]; - src.location.Offset(location_offset); - linked_ptr<api::automation_internal::AXNodeData> out_node( - new api::automation_internal::AXNodeData()); - PopulateNodeData(src, out_node); - if (src.HasBoolAttribute(ui::AX_ATTR_IS_AX_TREE_HOST)) { - const auto& iter = event.node_to_browser_plugin_instance_id_map.find( - src.id); - if (iter != event.node_to_browser_plugin_instance_id_map.end()) { - int instance_id = iter->second; - content::BrowserPluginGuestManager* guest_manager = - browser_context->GetGuestManager(); - content::WebContents* guest_web_contents = - guest_manager->GetGuestByInstanceID(event.process_id, - instance_id); - if (guest_web_contents) { - content::RenderFrameHost* guest_rfh = - guest_web_contents->GetMainFrame(); - int guest_tree_id = - AXTreeIDRegistry::GetInstance()->GetOrCreateAXTreeID( - guest_rfh->GetProcess()->GetID(), - guest_rfh->GetRoutingID()); - out_node->int_attributes->additional_properties.SetInteger( - ToString(ui::AX_ATTR_CHILD_TREE_ID), - guest_tree_id); - } - } - } - ax_tree_update.nodes.push_back(out_node); - } - - // TODO(dtseng/aboxhall): Why are we sending only one update at a time? We - // should match the behavior from renderer -> browser and send a - // collection of tree updates over (to the extension); see - // |AccessibilityHostMsg_EventParams| and |AccessibilityHostMsg_Events|. - DispatchEventInternal( - browser_context, events::AUTOMATION_INTERNAL_ON_ACCESSIBILITY_EVENT, - api::automation_internal::OnAccessibilityEvent::kEventName, - api::automation_internal::OnAccessibilityEvent::Create( - ax_event_params)); - } -} - -void DispatchTreeDestroyedEventToAutomation( - int process_id, - int routing_id, - content::BrowserContext* browser_context) { - int tree_id = AXTreeIDRegistry::GetInstance()->GetOrCreateAXTreeID( - process_id, routing_id); - DispatchEventInternal( - browser_context, - events::AUTOMATION_INTERNAL_ON_ACCESSIBILITY_TREE_DESTROYED, - api::automation_internal::OnAccessibilityTreeDestroyed::kEventName, - api::automation_internal::OnAccessibilityTreeDestroyed::Create(tree_id)); - AXTreeIDRegistry::GetInstance()->RemoveAXTreeID(tree_id); -} - -} // namespace automation_util - -} // namespace extensions diff --git a/chrome/browser/extensions/api/automation_internal/automation_util.h b/chrome/browser/extensions/api/automation_internal/automation_util.h deleted file mode 100644 index 218038b..0000000 --- a/chrome/browser/extensions/api/automation_internal/automation_util.h +++ /dev/null @@ -1,41 +0,0 @@ -// 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. - -#ifndef CHROME_BROWSER_EXTENSIONS_API_AUTOMATION_INTERNAL_AUTOMATION_UTIL_H_ -#define CHROME_BROWSER_EXTENSIONS_API_AUTOMATION_INTERNAL_AUTOMATION_UTIL_H_ - -#include <vector> - -#include "chrome/common/extensions/api/automation_internal.h" -#include "content/public/browser/ax_event_notification_details.h" - -namespace content { -class BrowserContext; -} // namespace content - -namespace ui { -struct AXNodeData; -} // namespace ui - -namespace extensions { - -// Shared utility functions for the Automation API. -namespace automation_util { - -// Dispatch events through the Automation API making any necessary conversions -// from accessibility to Automation types. -void DispatchAccessibilityEventsToAutomation( - const std::vector<content::AXEventNotificationDetails>& details, - content::BrowserContext* browser_context, - const gfx::Vector2d& location_offset); - -void DispatchTreeDestroyedEventToAutomation( - int process_id, - int routing_id, - content::BrowserContext* browser_context); -} // namespace automation_util - -} // namespace extensions - -#endif // CHROME_BROWSER_EXTENSIONS_API_AUTOMATION_INTERNAL_AUTOMATION_UTIL_H_ diff --git a/chrome/browser/resources/chromeos/chromevox/cvox2/background/output.js b/chrome/browser/resources/chromeos/chromevox/cvox2/background/output.js index 581f90c..8179d5b 100644 --- a/chrome/browser/resources/chromeos/chromevox/cvox2/background/output.js +++ b/chrome/browser/resources/chromeos/chromevox/cvox2/background/output.js @@ -440,7 +440,7 @@ Output.RULES = { }, textField: { speak: '$name $value $if(' + - '$type, @input_type_+$type, @input_type_text)', + '$inputType, @input_type_+$inputType, @input_type_text)', braille: '' }, toolbar: { diff --git a/chrome/browser/ui/aura/accessibility/automation_manager_aura.cc b/chrome/browser/ui/aura/accessibility/automation_manager_aura.cc index 5bc04e7..d1368d5 100644 --- a/chrome/browser/ui/aura/accessibility/automation_manager_aura.cc +++ b/chrome/browser/ui/aura/accessibility/automation_manager_aura.cc @@ -8,8 +8,9 @@ #include "base/memory/singleton.h" #include "chrome/browser/browser_process.h" -#include "chrome/browser/extensions/api/automation_internal/automation_util.h" +#include "chrome/browser/extensions/api/automation_internal/automation_event_router.h" #include "chrome/browser/profiles/profile_manager.h" +#include "chrome/common/extensions/chrome_extension_messages.h" #include "content/public/browser/ax_event_notification_details.h" #include "content/public/browser/browser_context.h" #include "ui/aura/window.h" @@ -19,6 +20,7 @@ #include "ui/views/widget/widget.h" using content::BrowserContext; +using extensions::AutomationEventRouter; // static AutomationManagerAura* AutomationManagerAura::GetInstance() { @@ -64,20 +66,7 @@ void AutomationManagerAura::HandleEvent(BrowserContext* context, views::AXAuraObjWrapper* aura_obj = views::AXAuraObjCache::GetInstance()->GetOrCreate(view); - - if (processing_events_) { - pending_events_.push_back(std::make_pair(aura_obj, event_type)); - return; - } - - processing_events_ = true; SendEvent(context, aura_obj, event_type); - - for (size_t i = 0; i < pending_events_.size(); ++i) - SendEvent(context, pending_events_[i].first, pending_events_[i].second); - - processing_events_ = false; - pending_events_.clear(); } void AutomationManagerAura::HandleAlert(content::BrowserContext* context, @@ -132,21 +121,28 @@ void AutomationManagerAura::ResetSerializer() { void AutomationManagerAura::SendEvent(BrowserContext* context, views::AXAuraObjWrapper* aura_obj, ui::AXEvent event_type) { - ui::AXTreeUpdate update; - current_tree_serializer_->SerializeChanges(aura_obj, &update); - - // Route this event to special process/routing ids recognized by the - // Automation API as the desktop tree. - // TODO(dtseng): Would idealy define these special desktop constants in idl. - content::AXEventNotificationDetails detail( - update.node_id_to_clear, update.nodes, event_type, aura_obj->GetID(), - std::map<int32, int>(), - 0, /* process_id */ - 0 /* routing_id */); - std::vector<content::AXEventNotificationDetails> details; - details.push_back(detail); - extensions::automation_util::DispatchAccessibilityEventsToAutomation( - details, context, gfx::Vector2d()); + if (processing_events_) { + pending_events_.push_back(std::make_pair(aura_obj, event_type)); + return; + } + processing_events_ = true; + + ExtensionMsg_AccessibilityEventParams params; + current_tree_serializer_->SerializeChanges(aura_obj, ¶ms.update); + params.tree_id = 0; + params.id = aura_obj->GetID(); + params.event_type = event_type; + AutomationEventRouter* router = AutomationEventRouter::GetInstance(); + router->DispatchAccessibilityEvent(params); + + processing_events_ = false; + auto pending_events_copy = pending_events_; + pending_events_.clear(); + for (size_t i = 0; i < pending_events_copy.size(); ++i) { + SendEvent(context, + pending_events_copy[i].first, + pending_events_copy[i].second); + } } void AutomationManagerAura::OnNativeFocusChanged(aura::Window* focused_now) { diff --git a/chrome/chrome_browser_extensions.gypi b/chrome/chrome_browser_extensions.gypi index 9602ac5..77f2c89 100644 --- a/chrome/chrome_browser_extensions.gypi +++ b/chrome/chrome_browser_extensions.gypi @@ -138,8 +138,6 @@ 'browser/extensions/api/automation_internal/automation_event_router.h', 'browser/extensions/api/automation_internal/automation_internal_api.cc', 'browser/extensions/api/automation_internal/automation_internal_api.h', - 'browser/extensions/api/automation_internal/automation_util.cc', - 'browser/extensions/api/automation_internal/automation_util.h', 'browser/extensions/api/autotest_private/autotest_private_api.cc', 'browser/extensions/api/autotest_private/autotest_private_api.h', 'browser/extensions/api/bookmark_manager_private/bookmark_manager_private_api.cc', diff --git a/chrome/common/extensions/api/automation.idl b/chrome/common/extensions/api/automation.idl index 51cdfe7..b49c3a5 100644 --- a/chrome/common/extensions/api/automation.idl +++ b/chrome/common/extensions/api/automation.idl @@ -318,8 +318,6 @@ // The role of this node. automation.RoleType role; - // TODO(aboxhall): expose states as mixins instead - // The $(ref:automation.StateType)s describing this node. object state; @@ -370,96 +368,21 @@ // name, via the $(ref:automation.AutomationNode.name) attribute. AutomationNode[] labelledBy; - // The nodes, if any, which are to be considered children of this node but - // are not children in the DOM tree. - AutomationNode[] owns; - - // TODO(aboxhall): Make this private? - - // A collection of this node's other attributes. - object? attributes; - - // The index of this node in its parent node's list of children. If this is - // the root node, this will be undefined. - long? indexInParent; - - AutomationNode[] children; - AutomationNode parent; - AutomationNode firstChild; - AutomationNode lastChild; - AutomationNode previousSibling; - AutomationNode nextSibling; - - // Does the default action based on this node's role. This is generally - // the same action that would result from clicking the node such as - // expanding a treeitem, toggling a checkbox, selecting a radiobutton, - // or activating a button. - static void doDefault(); - - // Places focus on this node. - static void focus(); - - // Scrolls this node to make it visible. - static void makeVisible(); - - // Sets selection within a text field. - static void setSelection(long startIndex, long endIndex); - - // Shows the context menu resulting from a right click on this node. - static void showContextMenu(); - - // Adds a listener for the given event type and event phase. - static void addEventListener( - EventType eventType, AutomationListener listener, boolean capture); - - // Removes a listener for the given event type and event phase. - static void removeEventListener( - EventType eventType, AutomationListener listener, boolean capture); - - // Gets the first node in this node's subtree which matches the given CSS - // selector and is within the same DOM context. - // - // If this node doesn't correspond directly with an HTML node in the DOM, - // querySelector will be run on this node's nearest HTML node ancestor. Note - // that this may result in the query returning a node which is not a - // descendant of this node. - // - // If the selector matches a node which doesn't directly correspond to an - // automation node (for example an element within an ARIA widget, where the - // ARIA widget forms one node of the automation tree, or an element which - // is hidden from accessibility via hiding it using CSS or using - // aria-hidden), this will return the nearest ancestor which does correspond - // to an automation node. - static void domQuerySelector(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); - - // Returns whether this node matches the given $(ref:automation.FindParams). - static boolean matches(FindParams params); - }; - - dictionary ActiveDescendantMixin { // The node referred to by <code>aria-activedescendant</code>, where // applicable AutomationNode activedescendant; - }; - // Attributes which are mixed in to an AutomationNode if it is a link. - dictionary LinkMixins { - // TODO(aboxhall): Add visited state + // + // Link attributes. + // // The URL that this link will navigate to. DOMString url; - }; - // Attributes which are mixed in to an AutomationNode if it is a document. - dictionary DocumentMixins { + // + // Document attributes. + // + // The URL of this document. DOMString docUrl; @@ -471,23 +394,22 @@ // The proportion (out of 1.0) that this doc has completed loading. double docLoadingProgress; - }; - // TODO(aboxhall): document ScrollableMixins (e.g. what is scrollXMin? is it - // ever not 0?) + // + // Scrollable container attributes. + // - // Attributes which are mixed in to an AutomationNode if it is scrollable. - dictionary ScrollableMixins { long scrollX; long scrollXMin; long scrollXMax; long scrollY; long scrollYMin; long scrollYMax; - }; - // Attributes which are mixed in to an AutomationNode if it is editable text. - dictionary EditableTextMixins { + // + // Editable text field attributes. + // + // The character index of the start of the selection within this editable // text element; -1 if no selection. long textSelStart; @@ -498,10 +420,11 @@ // The input type, like email or number. DOMString textInputType; - }; - // Attributes which are mixed in to an AutomationNode if it is a range. - dictionary RangeMixins { + // + // Range attributes. + // + // The current value for this range. double valueForRange; @@ -510,21 +433,21 @@ // The maximum possible value for this range. double maxValueForRange; - }; - // TODO(aboxhall): live region mixins. + // + // Table attributes. + // - // Attributes which are mixed in to an AutomationNode if it is a table. - dictionary TableMixins { // The number of rows in this table. long tableRowCount; // The number of columns in this table. long tableColumnCount; - }; - // Attributes which are mixed in to an AutomationNode if it is a table cell. - dictionary TableCellMixins { + // + // Table cell attributes. + // + // The zero-based index of the column that this cell is in. long tableCellColumnIndex; @@ -536,6 +459,75 @@ // The number of rows that this cell spans (default is 1). long tableCellRowSpan; + + // + // Walking the tree. + // + + AutomationNode[] children; + AutomationNode parent; + AutomationNode firstChild; + AutomationNode lastChild; + AutomationNode previousSibling; + AutomationNode nextSibling; + + // The index of this node in its parent node's list of children. If this is + // the root node, this will be undefined. + long? indexInParent; + + // + // Actions. + // + + // Does the default action based on this node's role. This is generally + // the same action that would result from clicking the node such as + // expanding a treeitem, toggling a checkbox, selecting a radiobutton, + // or activating a button. + static void doDefault(); + + // Places focus on this node. + static void focus(); + + // Scrolls this node to make it visible. + static void makeVisible(); + + // Sets selection within a text field. + static void setSelection(long startIndex, long endIndex); + + // Adds a listener for the given event type and event phase. + static void addEventListener( + EventType eventType, AutomationListener listener, boolean capture); + + // Removes a listener for the given event type and event phase. + static void removeEventListener( + EventType eventType, AutomationListener listener, boolean capture); + + // Gets the first node in this node's subtree which matches the given CSS + // selector and is within the same DOM context. + // + // If this node doesn't correspond directly with an HTML node in the DOM, + // querySelector will be run on this node's nearest HTML node ancestor. Note + // that this may result in the query returning a node which is not a + // descendant of this node. + // + // If the selector matches a node which doesn't directly correspond to an + // automation node (for example an element within an ARIA widget, where the + // ARIA widget forms one node of the automation tree, or an element which + // is hidden from accessibility via hiding it using CSS or using + // aria-hidden), this will return the nearest ancestor which does correspond + // to an automation node. + static void domQuerySelector(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); + + // Returns whether this node matches the given $(ref:automation.FindParams). + static boolean matches(FindParams params); }; // Called when the <code>AutomationNode</code> for the page is available. diff --git a/chrome/common/extensions/api/automation_internal.idl b/chrome/common/extensions/api/automation_internal.idl index b290c63..6f568cf 100644 --- a/chrome/common/extensions/api/automation_internal.idl +++ b/chrome/common/extensions/api/automation_internal.idl @@ -6,46 +6,10 @@ // essentially a translation of the internal accessibility tree update system // into an extension API. namespace automationInternal { - dictionary Rect { - long left; - long top; - long width; - long height; - }; - - // A compact representation of the accessibility information for a - // single web object, in a form that can be serialized and sent from - // one process to another. - // See ui/accessibility/ax_node_data.h - dictionary AXNodeData { - long id; - DOMString role; - object state; - Rect location; - - object? boolAttributes; - object? floatAttributes; - object? htmlAttributes; - object? intAttributes; - object? intlistAttributes; - object? stringAttributes; - long[] childIds; - }; - - dictionary AXTreeUpdate { - // ID of the node, if any, which should be invalidated before the new data - // is applied. - long nodeIdToClear; - - // A vector of nodes to update according to the rules described in - // ui/accessibility/ax_tree_update.h. - AXNodeData[] nodes; - }; - // Data for an accessibility event and/or an atomic change to an accessibility // tree. See ui/accessibility/ax_tree_update.h for an extended explanation of // the tree update format. - dictionary AXEventParams { + [nocompile] dictionary AXEventParams { // The tree id of the web contents that this update is for. long treeID; @@ -54,10 +18,6 @@ namespace automationInternal { // The type of event that this update represents. DOMString eventType; - - // Serialized changes to the tree structure and node data that should be - // applied before processing the event. - AXTreeUpdate update; }; // All possible actions that can be performed on automation nodes. @@ -133,5 +93,7 @@ namespace automationInternal { static void onAccessibilityEvent(AXEventParams update); static void onAccessibilityTreeDestroyed(long treeID); + + static void onTreeChange(long treeID, long nodeID, DOMString changeType); }; }; diff --git a/chrome/renderer/extensions/automation_internal_custom_bindings.cc b/chrome/renderer/extensions/automation_internal_custom_bindings.cc index 7f70eb5..632d07d 100644 --- a/chrome/renderer/extensions/automation_internal_custom_bindings.cc +++ b/chrome/renderer/extensions/automation_internal_custom_bindings.cc @@ -6,10 +6,10 @@ #include "base/bind.h" #include "base/memory/scoped_ptr.h" +#include "base/thread_task_runner_handle.h" #include "base/values.h" #include "chrome/common/extensions/chrome_extension_messages.h" #include "chrome/common/extensions/manifest_handlers/automation.h" -#include "content/public/child/v8_value_converter.h" #include "content/public/renderer/render_frame.h" #include "content/public/renderer/render_thread.h" #include "content/public/renderer/render_view.h" @@ -40,6 +40,9 @@ v8::Local<v8::Object> ToEnumObject(v8::Isolate* isolate, namespace extensions { +TreeCache::TreeCache() {} +TreeCache::~TreeCache() {} + class AutomationMessageFilter : public IPC::MessageFilter { public: explicit AutomationMessageFilter(AutomationInternalCustomBindings* owner) @@ -47,6 +50,7 @@ class AutomationMessageFilter : public IPC::MessageFilter { removed_(false) { DCHECK(owner); content::RenderThread::Get()->AddFilter(this); + task_runner_ = base::ThreadTaskRunnerHandle::Get(); } void Detach() { @@ -56,10 +60,15 @@ class AutomationMessageFilter : public IPC::MessageFilter { // IPC::MessageFilter bool OnMessageReceived(const IPC::Message& message) override { - if (owner_) - return owner_->OnMessageReceived(message); - else - return false; + task_runner_->PostTask( + FROM_HERE, + base::Bind( + &AutomationMessageFilter::OnMessageReceivedOnRenderThread, + this, message)); + + // Always return false in case there are multiple + // AutomationInternalCustomBindings instances attached to the same thread. + return false; } void OnFilterRemoved() override { @@ -67,6 +76,11 @@ class AutomationMessageFilter : public IPC::MessageFilter { } private: + void OnMessageReceivedOnRenderThread(const IPC::Message& message) { + if (owner_) + owner_->OnMessageReceived(message); + } + ~AutomationMessageFilter() override { Remove(); } @@ -80,6 +94,7 @@ private: AutomationInternalCustomBindings* owner_; bool removed_; + scoped_refptr<base::SingleThreadTaskRunner> task_runner_; DISALLOW_COPY_AND_ASSIGN(AutomationMessageFilter); }; @@ -89,35 +104,46 @@ AutomationInternalCustomBindings::AutomationInternalCustomBindings( // It's safe to use base::Unretained(this) here because these bindings // will only be called on a valid AutomationInternalCustomBindings instance // and none of the functions have any side effects. - RouteFunction( - "IsInteractPermitted", - base::Bind(&AutomationInternalCustomBindings::IsInteractPermitted, - base::Unretained(this))); - RouteFunction( - "GetSchemaAdditions", - base::Bind(&AutomationInternalCustomBindings::GetSchemaAdditions, - base::Unretained(this))); - RouteFunction( - "GetRoutingID", - base::Bind(&AutomationInternalCustomBindings::GetRoutingID, - base::Unretained(this))); - - message_filter_ = new AutomationMessageFilter(this); + #define ROUTE_FUNCTION(FN) \ + RouteFunction(#FN, \ + base::Bind(&AutomationInternalCustomBindings::FN, \ + base::Unretained(this))) + + ROUTE_FUNCTION(IsInteractPermitted); + ROUTE_FUNCTION(GetSchemaAdditions); + ROUTE_FUNCTION(GetRoutingID); + ROUTE_FUNCTION(StartCachingAccessibilityTrees); + ROUTE_FUNCTION(DestroyAccessibilityTree); + ROUTE_FUNCTION(GetRootID); + ROUTE_FUNCTION(GetParentID); + ROUTE_FUNCTION(GetChildCount); + ROUTE_FUNCTION(GetChildIDAtIndex); + ROUTE_FUNCTION(GetIndexInParent); + ROUTE_FUNCTION(GetState); + ROUTE_FUNCTION(GetRole); + ROUTE_FUNCTION(GetLocation); + ROUTE_FUNCTION(GetStringAttribute); + ROUTE_FUNCTION(GetBoolAttribute); + ROUTE_FUNCTION(GetIntAttribute); + ROUTE_FUNCTION(GetFloatAttribute); + ROUTE_FUNCTION(GetIntListAttribute); + ROUTE_FUNCTION(GetHtmlAttribute); + + #undef ROUTE_FUNCTION } AutomationInternalCustomBindings::~AutomationInternalCustomBindings() { - message_filter_->Detach(); + if (message_filter_) + message_filter_->Detach(); + STLDeleteContainerPairSecondPointers(tree_id_to_tree_cache_map_.begin(), + tree_id_to_tree_cache_map_.end()); } -bool AutomationInternalCustomBindings::OnMessageReceived( +void AutomationInternalCustomBindings::OnMessageReceived( const IPC::Message& message) { IPC_BEGIN_MESSAGE_MAP(AutomationInternalCustomBindings, message) IPC_MESSAGE_HANDLER(ExtensionMsg_AccessibilityEvent, OnAccessibilityEvent) IPC_END_MESSAGE_MAP() - - // Always return false in case there are multiple - // AutomationInternalCustomBindings instances attached to the same thread. - return false; } void AutomationInternalCustomBindings::IsInteractPermitted( @@ -136,6 +162,12 @@ void AutomationInternalCustomBindings::GetRoutingID( args.GetReturnValue().Set(v8::Integer::New(GetIsolate(), routing_id)); } +void AutomationInternalCustomBindings::StartCachingAccessibilityTrees( + const v8::FunctionCallbackInfo<v8::Value>& args) { + if (!message_filter_) + message_filter_ = new AutomationMessageFilter(this); +} + void AutomationInternalCustomBindings::GetSchemaAdditions( const v8::FunctionCallbackInfo<v8::Value>& args) { v8::Local<v8::Object> additions = v8::Object::New(GetIsolate()); @@ -159,9 +191,438 @@ void AutomationInternalCustomBindings::GetSchemaAdditions( args.GetReturnValue().Set(additions); } +void AutomationInternalCustomBindings::DestroyAccessibilityTree( + const v8::FunctionCallbackInfo<v8::Value>& args) { + if (args.Length() != 1 || !args[0]->IsNumber()) { + ThrowInvalidArgumentsException(args); + return; + } + + int tree_id = args[0]->Int32Value(); + auto iter = tree_id_to_tree_cache_map_.find(tree_id); + if (iter == tree_id_to_tree_cache_map_.end()) + return; + + TreeCache* cache = iter->second; + tree_id_to_tree_cache_map_.erase(tree_id); + axtree_to_tree_cache_map_.erase(&cache->tree); + delete cache; +} + +// +// Access the cached accessibility trees and properties of their nodes. +// + +void AutomationInternalCustomBindings::GetRootID( + const v8::FunctionCallbackInfo<v8::Value>& args) { + if (args.Length() != 1 || !args[0]->IsNumber()) { + ThrowInvalidArgumentsException(args); + return; + } + + int tree_id = args[0]->Int32Value(); + const auto iter = tree_id_to_tree_cache_map_.find(tree_id); + if (iter == tree_id_to_tree_cache_map_.end()) + return; + + TreeCache* cache = iter->second; + int root_id = cache->tree.root()->id(); + args.GetReturnValue().Set(v8::Integer::New(GetIsolate(), root_id)); +} + +void AutomationInternalCustomBindings::GetParentID( + const v8::FunctionCallbackInfo<v8::Value>& args) { + ui::AXNode* node = nullptr; + if (!GetNodeHelper(args, nullptr, &node)) + return; + + if (!node->parent()) + return; + + int parent_id = node->parent()->id(); + args.GetReturnValue().Set(v8::Integer::New(GetIsolate(), parent_id)); +} + +void AutomationInternalCustomBindings::GetChildCount( + const v8::FunctionCallbackInfo<v8::Value>& args) { + ui::AXNode* node = nullptr; + if (!GetNodeHelper(args, nullptr, &node)) + return; + + int child_count = node->child_count(); + args.GetReturnValue().Set(v8::Integer::New(GetIsolate(), child_count)); +} + +void AutomationInternalCustomBindings::GetChildIDAtIndex( + const v8::FunctionCallbackInfo<v8::Value>& args) { + if (args.Length() < 3 || !args[2]->IsNumber()) { + ThrowInvalidArgumentsException(args); + return; + } + + ui::AXNode* node = nullptr; + if (!GetNodeHelper(args, nullptr, &node)) + return; + + int index = args[2]->Int32Value(); + if (index < 0 || index >= node->child_count()) + return; + + int child_id = node->children()[index]->id(); + args.GetReturnValue().Set(v8::Integer::New(GetIsolate(), child_id)); +} + +void AutomationInternalCustomBindings::GetIndexInParent( + const v8::FunctionCallbackInfo<v8::Value>& args) { + ui::AXNode* node = nullptr; + if (!GetNodeHelper(args, nullptr, &node)) + return; + + int index_in_parent = node->index_in_parent(); + args.GetReturnValue().Set(v8::Integer::New(GetIsolate(), index_in_parent)); +} + +void AutomationInternalCustomBindings::GetState( + const v8::FunctionCallbackInfo<v8::Value>& args) { + ui::AXNode* node = nullptr; + if (!GetNodeHelper(args, nullptr, &node)) + return; + + v8::Local<v8::Object> state(v8::Object::New(GetIsolate())); + uint32 state_pos = 0, state_shifter = node->data().state; + while (state_shifter) { + if (state_shifter & 1) { + std::string key = ToString(static_cast<ui::AXState>(state_pos)); + state->Set(CreateV8String(key), + v8::Boolean::New(GetIsolate(), true)); + } + state_shifter = state_shifter >> 1; + state_pos++; + } + + args.GetReturnValue().Set(state); +} + +void AutomationInternalCustomBindings::GetRole( + const v8::FunctionCallbackInfo<v8::Value>& args) { + ui::AXNode* node = nullptr; + if (!GetNodeHelper(args, nullptr, &node)) + return; + + std::string role_name = ui::ToString(node->data().role); + args.GetReturnValue().Set( + v8::String::NewFromUtf8(GetIsolate(), role_name.c_str())); +} + +void AutomationInternalCustomBindings::GetLocation( + const v8::FunctionCallbackInfo<v8::Value>& args) { + TreeCache* cache; + ui::AXNode* node = nullptr; + if (!GetNodeHelper(args, &cache, &node)) + return; + + v8::Local<v8::Object> location_obj(v8::Object::New(GetIsolate())); + gfx::Rect location = node->data().location; + location.Offset(cache->location_offset); + location_obj->Set(CreateV8String("left"), + v8::Integer::New(GetIsolate(), location.x())); + location_obj->Set(CreateV8String("top"), + v8::Integer::New(GetIsolate(), location.y())); + location_obj->Set(CreateV8String("width"), + v8::Integer::New(GetIsolate(), location.width())); + location_obj->Set(CreateV8String("height"), + v8::Integer::New(GetIsolate(), location.height())); + args.GetReturnValue().Set(location_obj); +} + +void AutomationInternalCustomBindings::GetStringAttribute( + const v8::FunctionCallbackInfo<v8::Value>& args) { + ui::AXNode* node = nullptr; + std::string attribute_name; + if (!GetAttributeHelper(args, &node, &attribute_name)) + return; + + ui::AXStringAttribute attribute = ui::ParseAXStringAttribute(attribute_name); + std::string attr_value; + if (!node->data().GetStringAttribute(attribute, &attr_value)) + return; + + args.GetReturnValue().Set( + v8::String::NewFromUtf8(GetIsolate(), attr_value.c_str())); +} + +void AutomationInternalCustomBindings::GetBoolAttribute( + const v8::FunctionCallbackInfo<v8::Value>& args) { + ui::AXNode* node = nullptr; + std::string attribute_name; + if (!GetAttributeHelper(args, &node, &attribute_name)) + return; + + ui::AXBoolAttribute attribute = ui::ParseAXBoolAttribute(attribute_name); + bool attr_value; + if (!node->data().GetBoolAttribute(attribute, &attr_value)) + return; + + args.GetReturnValue().Set(v8::Boolean::New(GetIsolate(), attr_value)); +} + +void AutomationInternalCustomBindings::GetIntAttribute( + const v8::FunctionCallbackInfo<v8::Value>& args) { + ui::AXNode* node = nullptr; + std::string attribute_name; + if (!GetAttributeHelper(args, &node, &attribute_name)) + return; + + ui::AXIntAttribute attribute = ui::ParseAXIntAttribute(attribute_name); + int attr_value; + if (!node->data().GetIntAttribute(attribute, &attr_value)) + return; + + args.GetReturnValue().Set(v8::Integer::New(GetIsolate(), attr_value)); +} + +void AutomationInternalCustomBindings::GetFloatAttribute( + const v8::FunctionCallbackInfo<v8::Value>& args) { + ui::AXNode* node = nullptr; + std::string attribute_name; + if (!GetAttributeHelper(args, &node, &attribute_name)) + return; + + ui::AXFloatAttribute attribute = ui::ParseAXFloatAttribute(attribute_name); + float attr_value; + + if (!node->data().GetFloatAttribute(attribute, &attr_value)) + return; + + args.GetReturnValue().Set(v8::Number::New(GetIsolate(), attr_value)); +} + +void AutomationInternalCustomBindings::GetIntListAttribute( + const v8::FunctionCallbackInfo<v8::Value>& args) { + ui::AXNode* node = nullptr; + std::string attribute_name; + if (!GetAttributeHelper(args, &node, &attribute_name)) + return; + + ui::AXIntListAttribute attribute = + ui::ParseAXIntListAttribute(attribute_name); + if (!node->data().HasIntListAttribute(attribute)) + return; + const std::vector<int32>& attr_value = + node->data().GetIntListAttribute(attribute); + + v8::Local<v8::Array> result(v8::Array::New(GetIsolate(), attr_value.size())); + for (size_t i = 0; i < attr_value.size(); ++i) + result->Set(static_cast<uint32>(i), + v8::Integer::New(GetIsolate(), attr_value[i])); + args.GetReturnValue().Set(result); +} + +void AutomationInternalCustomBindings::GetHtmlAttribute( + const v8::FunctionCallbackInfo<v8::Value>& args) { + ui::AXNode* node = nullptr; + std::string attribute_name; + if (!GetAttributeHelper(args, &node, &attribute_name)) + return; + + std::string attr_value; + if (!node->data().GetHtmlAttribute(attribute_name.c_str(), &attr_value)) + return; + + args.GetReturnValue().Set( + v8::String::NewFromUtf8(GetIsolate(), attr_value.c_str())); +} + +// +// Helper functions. +// + +void AutomationInternalCustomBindings::ThrowInvalidArgumentsException( + const v8::FunctionCallbackInfo<v8::Value>& args) { + GetIsolate()->ThrowException( + v8::String::NewFromUtf8( + GetIsolate(), + "Invalid arguments to AutomationInternalCustomBindings function", + v8::NewStringType::kNormal).ToLocalChecked()); + + LOG(FATAL) + << "Invalid arguments to AutomationInternalCustomBindings function" + << context()->GetStackTraceAsString(); +} + +bool AutomationInternalCustomBindings::GetNodeHelper( + const v8::FunctionCallbackInfo<v8::Value>& args, + TreeCache** out_cache, + ui::AXNode** out_node) { + if (args.Length() < 2 || !args[0]->IsNumber() || !args[1]->IsNumber()) { + ThrowInvalidArgumentsException(args); + return false; + } + + int tree_id = args[0]->Int32Value(); + int node_id = args[1]->Int32Value(); + + const auto iter = tree_id_to_tree_cache_map_.find(tree_id); + if (iter == tree_id_to_tree_cache_map_.end()) + return false; + + TreeCache* cache = iter->second; + ui::AXNode* node = cache->tree.GetFromId(node_id); + + if (out_cache) + *out_cache = cache; + if (out_node) + *out_node = node; + + return node != nullptr; +} + +bool AutomationInternalCustomBindings::GetAttributeHelper( + const v8::FunctionCallbackInfo<v8::Value>& args, + ui::AXNode** out_node, + std::string* out_attribute_name) { + if (args.Length() != 3 || + !args[2]->IsString()) { + ThrowInvalidArgumentsException(args); + return false; + } + + TreeCache* cache = nullptr; + if (!GetNodeHelper(args, &cache, out_node)) + return false; + + *out_attribute_name = *v8::String::Utf8Value(args[2]); + return true; +} + +v8::Local<v8::Value> AutomationInternalCustomBindings::CreateV8String( + const char* str) { + return v8::String::NewFromUtf8( + GetIsolate(), str, v8::String::kNormalString, strlen(str)); +} + +v8::Local<v8::Value> AutomationInternalCustomBindings::CreateV8String( + const std::string& str) { + return v8::String::NewFromUtf8( + GetIsolate(), str.c_str(), v8::String::kNormalString, str.length()); +} + +// +// Handle accessibility events from the browser process. +// + void AutomationInternalCustomBindings::OnAccessibilityEvent( const ExtensionMsg_AccessibilityEventParams& params) { - // TODO(dmazzoni): finish implementing this. + int tree_id = params.tree_id; + TreeCache* cache; + auto iter = tree_id_to_tree_cache_map_.find(tree_id); + if (iter == tree_id_to_tree_cache_map_.end()) { + cache = new TreeCache(); + cache->tab_id = -1; + cache->tree_id = params.tree_id; + cache->tree.SetDelegate(this); + tree_id_to_tree_cache_map_.insert(std::make_pair(tree_id, cache)); + axtree_to_tree_cache_map_.insert(std::make_pair(&cache->tree, cache)); + } else { + cache = iter->second; + } + + cache->location_offset = params.location_offset; + if (!cache->tree.Unserialize(params.update)) { + LOG(FATAL) << cache->tree.error(); + return; + } + + v8::HandleScope handle_scope(GetIsolate()); + v8::Context::Scope context_scope(context()->v8_context()); + v8::Local<v8::Array> args(v8::Array::New(GetIsolate(), 1U)); + v8::Local<v8::Object> event_params(v8::Object::New(GetIsolate())); + event_params->Set(CreateV8String("treeID"), + v8::Integer::New(GetIsolate(), params.tree_id)); + event_params->Set(CreateV8String("targetID"), + v8::Integer::New(GetIsolate(), params.id)); + event_params->Set(CreateV8String("eventType"), + CreateV8String(ToString(params.event_type))); + args->Set(0U, event_params); + context()->DispatchEvent("automationInternal.onAccessibilityEvent", args); +} + +void AutomationInternalCustomBindings::OnNodeWillBeDeleted(ui::AXTree* tree, + ui::AXNode* node) { + SendTreeChangeEvent( + api::automation::TREE_CHANGE_TYPE_NODEREMOVED, + tree, node); +} + +void AutomationInternalCustomBindings::OnSubtreeWillBeDeleted( + ui::AXTree* tree, + ui::AXNode* node) { + // This isn't strictly needed, as OnNodeWillBeDeleted will already be + // called. We could send a JS event for this only if it turns out to + // be needed for something. +} + +void AutomationInternalCustomBindings::OnNodeCreated(ui::AXTree* tree, + ui::AXNode* node) { + // Not needed, this is called in the middle of an update so it's not + // safe to trigger JS from here. Wait for the notification in + // OnAtomicUpdateFinished instead. +} + +void AutomationInternalCustomBindings::OnNodeChanged(ui::AXTree* tree, + ui::AXNode* node) { + // Not needed, this is called in the middle of an update so it's not + // safe to trigger JS from here. Wait for the notification in + // OnAtomicUpdateFinished instead. +} + +void AutomationInternalCustomBindings::OnAtomicUpdateFinished( + ui::AXTree* tree, + bool root_changed, + const std::vector<ui::AXTreeDelegate::Change>& changes) { + auto iter = axtree_to_tree_cache_map_.find(tree); + if (iter == axtree_to_tree_cache_map_.end()) + return; + + for (auto change : changes) { + ui::AXNode* node = change.node; + switch (change.type) { + case NODE_CREATED: + SendTreeChangeEvent( + api::automation::TREE_CHANGE_TYPE_NODECREATED, + tree, node); + break; + case SUBTREE_CREATED: + SendTreeChangeEvent( + api::automation::TREE_CHANGE_TYPE_SUBTREECREATED, + tree, node); + break; + case NODE_CHANGED: + SendTreeChangeEvent( + api::automation::TREE_CHANGE_TYPE_NODECHANGED, + tree, node); + break; + } + } +} + +void AutomationInternalCustomBindings::SendTreeChangeEvent( + api::automation::TreeChangeType change_type, + ui::AXTree* tree, + ui::AXNode* node) { + auto iter = axtree_to_tree_cache_map_.find(tree); + if (iter == axtree_to_tree_cache_map_.end()) + return; + + int tree_id = iter->second->tree_id; + + v8::HandleScope handle_scope(GetIsolate()); + v8::Context::Scope context_scope(context()->v8_context()); + v8::Local<v8::Array> args(v8::Array::New(GetIsolate(), 3U)); + args->Set(0U, v8::Integer::New(GetIsolate(), tree_id)); + args->Set(1U, v8::Integer::New(GetIsolate(), node->id())); + args->Set(2U, CreateV8String(ToString(change_type))); + context()->DispatchEvent("automationInternal.onTreeChange", args); } } // namespace extensions diff --git a/chrome/renderer/extensions/automation_internal_custom_bindings.h b/chrome/renderer/extensions/automation_internal_custom_bindings.h index 1bc8a79..c4f2cd2 100644 --- a/chrome/renderer/extensions/automation_internal_custom_bindings.h +++ b/chrome/renderer/extensions/automation_internal_custom_bindings.h @@ -6,6 +6,7 @@ #define CHROME_RENDERER_EXTENSIONS_AUTOMATION_INTERNAL_CUSTOM_BINDINGS_H_ #include "base/compiler_specific.h" +#include "chrome/common/extensions/api/automation.h" #include "extensions/renderer/object_backed_native_handler.h" #include "ipc/ipc_message.h" #include "ui/accessibility/ax_tree.h" @@ -17,15 +18,27 @@ namespace extensions { class AutomationMessageFilter; +struct TreeCache { + TreeCache(); + ~TreeCache(); + + int tab_id; + int tree_id; + + gfx::Vector2d location_offset; + ui::AXTree tree; +}; + // The native component of custom bindings for the chrome.automationInternal // API. -class AutomationInternalCustomBindings : public ObjectBackedNativeHandler { +class AutomationInternalCustomBindings : public ObjectBackedNativeHandler, + public ui::AXTreeDelegate { public: explicit AutomationInternalCustomBindings(ScriptContext* context); ~AutomationInternalCustomBindings() override; - bool OnMessageReceived(const IPC::Message& message); + void OnMessageReceived(const IPC::Message& message); private: // Returns whether this extension has the "interact" permission set (either @@ -39,10 +52,120 @@ class AutomationInternalCustomBindings : public ObjectBackedNativeHandler { // Get the routing ID for the extension. void GetRoutingID(const v8::FunctionCallbackInfo<v8::Value>& args); + // This is called by automation_internal_custom_bindings.js to indicate + // that an API was called that needs access to accessibility trees. This + // enables the MessageFilter that allows us to listen to accessibility + // events forwarded to this process. + void StartCachingAccessibilityTrees( + const v8::FunctionCallbackInfo<v8::Value>& args); + + // Called when an accessibility tree is destroyed and needs to be + // removed from our cache. + // Args: int ax_tree_id + void DestroyAccessibilityTree( + const v8::FunctionCallbackInfo<v8::Value>& args); + + // + // Access the cached accessibility trees and properties of their nodes. + // + + // Args: int ax_tree_id, Returns: int root_node_id. + void GetRootID(const v8::FunctionCallbackInfo<v8::Value>& args); + + // Args: int ax_tree_id, int node_id, Returns: int parent_node_id. + void GetParentID(const v8::FunctionCallbackInfo<v8::Value>& args); + + // Args: int ax_tree_id, int node_id, Returns: int child_count. + void GetChildCount(const v8::FunctionCallbackInfo<v8::Value>& args); + + // Args: int ax_tree_id, int node_id, Returns: int child_id. + void GetChildIDAtIndex(const v8::FunctionCallbackInfo<v8::Value>& args); + + // Args: int ax_tree_id, int node_id, Returns: int index_in_parent. + void GetIndexInParent(const v8::FunctionCallbackInfo<v8::Value>& args); + + // Args: int ax_tree_id, int node_id + // Returns: JS object with a string key for each state flag that's set. + void GetState(const v8::FunctionCallbackInfo<v8::Value>& args); + + // Args: int ax_tree_id, int node_id, Returns: string role_name + void GetRole(const v8::FunctionCallbackInfo<v8::Value>& args); + + // Args: int ax_tree_id, int node_id + // Returns: JS object with {left, top, width, height} + void GetLocation(const v8::FunctionCallbackInfo<v8::Value>& args); + + // Args: int ax_tree_id, int node_id, string attribute_name + // Returns: string attribute_value. + void GetStringAttribute(const v8::FunctionCallbackInfo<v8::Value>& args); + + // Args: int ax_tree_id, int node_id, string attribute_name + // Returns: bool attribute_value. + void GetBoolAttribute(const v8::FunctionCallbackInfo<v8::Value>& args); + + // Args: int ax_tree_id, int node_id, string attribute_name + // Returns: int attribute_value. + void GetIntAttribute(const v8::FunctionCallbackInfo<v8::Value>& args); + + // Args: int ax_tree_id, int node_id, string attribute_name + // Returns: float attribute_value. + void GetFloatAttribute(const v8::FunctionCallbackInfo<v8::Value>& args); + + // Args: int ax_tree_id, int node_id, string attribute_name + // Returns: JS array of int attribute_values. + void GetIntListAttribute(const v8::FunctionCallbackInfo<v8::Value>& args); + + // Args: int ax_tree_id, int node_id, string attribute_name + // Returns: string attribute_value. + void GetHtmlAttribute(const v8::FunctionCallbackInfo<v8::Value>& args); + + // + // Helper functions. + // + + // Throw an exception if we get bad arguments. + void ThrowInvalidArgumentsException( + const v8::FunctionCallbackInfo<v8::Value>& args); + + // For any function that takes int ax_tree_id, int node_id as its first + // two arguments, returns the tree and node it corresponds to, or returns + // false if not found. + bool GetNodeHelper( + const v8::FunctionCallbackInfo<v8::Value>& args, + TreeCache** out_cache, + ui::AXNode** out_node); + + // For any function that takes int ax_tree_id, int node_id, string attr + // as its first, returns the node it corresponds to and the string as + // a UTF8 string. + bool GetAttributeHelper( + const v8::FunctionCallbackInfo<v8::Value>& args, + ui::AXNode** out_node, + std::string* out_attribute_name); + + // Create a V8 string from a string. + v8::Local<v8::Value> CreateV8String(const char* str); + v8::Local<v8::Value> CreateV8String(const std::string& str); + // Handle accessibility events from the browser process. void OnAccessibilityEvent( const ExtensionMsg_AccessibilityEventParams& params); + // AXTreeDelegate implementation. + void OnNodeWillBeDeleted(ui::AXTree* tree, ui::AXNode* node) override; + void OnSubtreeWillBeDeleted(ui::AXTree* tree, ui::AXNode* node) override; + void OnNodeCreated(ui::AXTree* tree, ui::AXNode* node) override; + void OnNodeChanged(ui::AXTree* tree, ui::AXNode* node) override; + void OnAtomicUpdateFinished(ui::AXTree* tree, + bool root_changed, + const std::vector<Change>& changes) override; + + void SendTreeChangeEvent(api::automation::TreeChangeType change_type, + ui::AXTree* tree, + ui::AXNode* node); + + base::hash_map<int, TreeCache*> tree_id_to_tree_cache_map_; + base::hash_map<ui::AXTree*, TreeCache*> axtree_to_tree_cache_map_; scoped_refptr<AutomationMessageFilter> message_filter_; DISALLOW_COPY_AND_ASSIGN(AutomationInternalCustomBindings); diff --git a/chrome/renderer/resources/extensions/automation/automation_node.js b/chrome/renderer/resources/extensions/automation/automation_node.js index 5dc6b9b..e5de712 100644 --- a/chrome/renderer/resources/extensions/automation/automation_node.js +++ b/chrome/renderer/resources/extensions/automation/automation_node.js @@ -8,6 +8,124 @@ var automationInternal = var IsInteractPermitted = requireNative('automationInternal').IsInteractPermitted; +/** + * @param {number} axTreeID The id of the accessibility tree. + * @return {?number} The id of the root node. + */ +var GetRootID = requireNative('automationInternal').GetRootID; + +/** + * @param {number} axTreeID The id of the accessibility tree. + * @param {number} nodeID The id of a node. + * @return {?number} The id of the node's parent, or undefined if it's the + * root of its tree or if the tree or node wasn't found. + */ +var GetParentID = requireNative('automationInternal').GetParentID; + +/** + * @param {number} axTreeID The id of the accessibility tree. + * @param {number} nodeID The id of a node. + * @return {?number} The number of children of the node, or undefined if + * the tree or node wasn't found. + */ +var GetChildCount = requireNative('automationInternal').GetChildCount; + +/** + * @param {number} axTreeID The id of the accessibility tree. + * @param {number} nodeID The id of a node. + * @param {number} childIndex An index of a child of this node. + * @return {?number} The id of the child at the given index, or undefined + * if the tree or node or child at that index wasn't found. + */ +var GetChildIDAtIndex = requireNative('automationInternal').GetChildIDAtIndex; + +/** + * @param {number} axTreeID The id of the accessibility tree. + * @param {number} nodeID The id of a node. + * @return {?number} The index of this node in its parent, or undefined if + * the tree or node or node parent wasn't found. + */ +var GetIndexInParent = requireNative('automationInternal').GetIndexInParent; + +/** + * @param {number} axTreeID The id of the accessibility tree. + * @param {number} nodeID The id of a node. + * @return {?Object} An object with a string key for every state flag set, + * or undefined if the tree or node or node parent wasn't found. + */ +var GetState = requireNative('automationInternal').GetState; + +/** + * @param {number} axTreeID The id of the accessibility tree. + * @param {number} nodeID The id of a node. + * @return {string} The role of the node, or undefined if the tree or + * node wasn't found. + */ +var GetRole = requireNative('automationInternal').GetRole; + +/** + * @param {number} axTreeID The id of the accessibility tree. + * @param {number} nodeID The id of a node. + * @return {?automation.Rect} The location of the node, or undefined if + * the tree or node wasn't found. + */ +var GetLocation = requireNative('automationInternal').GetLocation; + +/** + * @param {number} axTreeID The id of the accessibility tree. + * @param {number} nodeID The id of a node. + * @param {string} attr The name of a string attribute. + * @return {?string} The value of this attribute, or undefined if the tree, + * node, or attribute wasn't found. + */ +var GetStringAttribute = requireNative('automationInternal').GetStringAttribute; + +/** + * @param {number} axTreeID The id of the accessibility tree. + * @param {number} nodeID The id of a node. + * @param {string} attr The name of an attribute. + * @return {?boolean} The value of this attribute, or undefined if the tree, + * node, or attribute wasn't found. + */ +var GetBoolAttribute = requireNative('automationInternal').GetBoolAttribute; + +/** + * @param {number} axTreeID The id of the accessibility tree. + * @param {number} nodeID The id of a node. + * @param {string} attr The name of an attribute. + * @return {?number} The value of this attribute, or undefined if the tree, + * node, or attribute wasn't found. + */ +var GetIntAttribute = requireNative('automationInternal').GetIntAttribute; + +/** + * @param {number} axTreeID The id of the accessibility tree. + * @param {number} nodeID The id of a node. + * @param {string} attr The name of an attribute. + * @return {?number} The value of this attribute, or undefined if the tree, + * node, or attribute wasn't found. + */ +var GetFloatAttribute = requireNative('automationInternal').GetFloatAttribute; + +/** + * @param {number} axTreeID The id of the accessibility tree. + * @param {number} nodeID The id of a node. + * @param {string} attr The name of an attribute. + * @return {?Array.<number>} The value of this attribute, or undefined + * if the tree, node, or attribute wasn't found. + */ +var GetIntListAttribute = + requireNative('automationInternal').GetIntListAttribute; + +/** + * @param {number} axTreeID The id of the accessibility tree. + * @param {number} nodeID The id of a node. + * @param {string} attr The name of an HTML attribute. + * @return {?string} The value of this attribute, or undefined if the tree, + * node, or attribute wasn't found. + */ +var GetHtmlAttribute = requireNative('automationInternal').GetHtmlAttribute; + var lastError = require('lastError'); var logging = requireNative('logging'); var schema = requireNative('automationInternal').GetSchemaAdditions(); @@ -20,16 +138,12 @@ var utils = require('utils'); */ 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 = { + treeID: -1, id: -1, role: '', state: { busy: true }, @@ -40,16 +154,51 @@ AutomationNodeImpl.prototype = { }, get parent() { - return this.hostTree || this.rootImpl.get(this.parentID); + if (this.hostNode_) + return this.hostNode_; + var parentID = GetParentID(this.treeID, this.id); + return this.rootImpl.get(parentID); + }, + + get state() { + return GetState(this.treeID, this.id); + }, + + get role() { + return GetRole(this.treeID, this.id); + }, + + get location() { + return GetLocation(this.treeID, this.id); + }, + + get indexInParent() { + return GetIndexInParent(this.treeID, this.id); + }, + + get childTree() { + var childTreeID = GetIntAttribute(this.treeID, this.id, 'childTreeId'); + if (childTreeID) + return AutomationRootNodeImpl.get(childTreeID); }, get firstChild() { - return this.childTree || this.rootImpl.get(this.childIds[0]); + if (this.childTree) + return this.childTree; + if (!GetChildCount(this.treeID, this.id)) + return undefined; + var firstChildID = GetChildIDAtIndex(this.treeID, this.id, 0); + return this.rootImpl.get(firstChildID); }, get lastChild() { - var childIds = this.childIds; - return this.childTree || this.rootImpl.get(childIds[childIds.length - 1]); + if (this.childTree) + return this.childTree; + var count = GetChildCount(this.treeID, this.id); + if (!count) + return undefined; + var lastChildID = GetChildIDAtIndex(this.treeID, this.id, count - 1); + return this.rootImpl.get(lastChildID); }, get children() { @@ -57,24 +206,28 @@ AutomationNodeImpl.prototype = { 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)); + var count = GetChildCount(this.treeID, this.id); + for (var i = 0; i < count; ++i) { + var childID = GetChildIDAtIndex(this.treeID, this.id, i); + var child = this.rootImpl.get(childID); + children.push(child); } return children; }, get previousSibling() { var parent = this.parent; - if (parent && this.indexInParent > 0) - return parent.children[this.indexInParent - 1]; + var indexInParent = GetIndexInParent(this.treeID, this.id); + if (parent && indexInParent > 0) + return parent.children[indexInParent - 1]; return undefined; }, get nextSibling() { var parent = this.parent; - if (parent && this.indexInParent < parent.children.length) - return parent.children[this.indexInParent + 1]; + var indexInParent = GetIndexInParent(this.treeID, this.id); + if (parent && indexInParent < parent.children.length) + return parent.children[indexInParent + 1]; return undefined; }, @@ -170,12 +323,20 @@ AutomationNodeImpl.prototype = { var impl = privates(this).impl; if (!impl) impl = this; + + var parentID = GetParentID(this.treeID, this.id); + var count = GetChildCount(this.treeID, this.id); + var childIDs = []; + for (var i = 0; i < count; ++i) { + var childID = GetChildIDAtIndex(this.treeID, this.id, i); + childIDs.push(childID); + } + 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); + ' parentID=' + parentID + + ' childIds=' + $JSON.stringify(childIDs); }, dispatchEventAtCapturing_: function(event, path) { @@ -219,9 +380,9 @@ AutomationNodeImpl.prototype = { 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); + logging.WARNING('Error in event handler for ' + event.type + + ' during phase ' + eventPhase + ': ' + + e.message + '\nStack trace: ' + e.stack); } } }, @@ -312,17 +473,14 @@ AutomationNodeImpl.prototype = { } 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) + if (this[attribute] !== attrValue) return false; } else if (attrValue instanceof RegExp) { - if (typeof this.attributesInternal[attribute] != 'string') + if (typeof this[attribute] != 'string') return false; - if (!attrValue.test(this.attributesInternal[attribute])) + if (!attrValue.test(this[attribute])) return false; } else { // TODO(aboxhall): handle intlist case. @@ -334,171 +492,196 @@ AutomationNodeImpl.prototype = { } }; -// 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>} - * @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<boolean>} - * @const - */ -var ATTRIBUTE_BLACKLIST = {'activedescendantId': true, - 'childTreeId': true, - 'controlsIds': true, - 'describedbyIds': true, - 'flowtoIds': true, - 'labelledbyIds': true, - 'ownsIds': true -}; - -function defaultStringAttribute(opt_defaultVal) { - return { default: undefined, 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(), - htmlTag: defaultStringAttribute(), - hierarchicalLevel: defaultIntAttribute(), - controls: defaultNodeRefListAttribute('controlsIds'), - describedby: defaultNodeRefListAttribute('describedbyIds'), - flowto: defaultNodeRefListAttribute('flowtoIds'), - labelledby: defaultNodeRefListAttribute('labelledbyIds'), - owns: defaultNodeRefListAttribute('ownsIds'), - wordStarts: defaultIntListAttribute(), - wordEnds: defaultIntListAttribute() -}; - -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), - type: defaultHtmlAttribute() -}; - -var RangeMixinAttributes = { - valueForRange: defaultFloatAttribute(), - minValueForRange: defaultFloatAttribute(), - maxValueForRange: defaultFloatAttribute() -}; - -var TableMixinAttributes = { - tableRowCount: defaultIntAttribute(), - tableColumnCount: defaultIntAttribute() -}; +var stringAttributes = [ + 'accessKey', + 'action', + 'ariaInvalidValue', + 'autoComplete', + 'containerLiveRelevant', + 'containerLiveStatus', + 'description', + 'display', + 'docDoctype', + 'docMimetype', + 'docTitle', + 'docUrl', + 'dropeffect', + 'help', + 'htmlTag', + 'liveRelevant', + 'liveStatus', + 'name', + 'placeholder', + 'shortcut', + 'textInputType', + 'url', + 'value']; + +var boolAttributes = [ + 'ariaReadonly', + 'buttonMixed', + 'canSetValue', + 'canvasHasFallback', + 'containerLiveAtomic', + 'containerLiveBusy', + 'docLoaded', + 'grabbed', + 'isAxTreeHost', + 'liveAtomic', + 'liveBusy', + 'updateLocationOnly']; + +var intAttributes = [ + 'backgroundColor', + 'color', + 'colorValue', + 'hierarchicalLevel', + 'invalidState', + 'posInSet', + 'scrollX', + 'scrollXMax', + 'scrollXMin', + 'scrollY', + 'scrollYMax', + 'scrollYMin', + 'setSize', + 'sortDirection', + 'tableCellColumnIndex', + 'tableCellColumnSpan', + 'tableCellRowIndex', + 'tableCellRowSpan', + 'tableColumnCount', + 'tableColumnIndex', + 'tableRowCount', + 'tableRowIndex', + 'textDirection', + 'textSelEnd', + 'textSelStart', + 'textStyle']; + +var nodeRefAttributes = [ + ['activedescendantId', 'activedescendant'], + ['tableColumnHeaderId', 'tableColumnHeader'], + ['tableHeaderId', 'tableHeader'], + ['tableRowHeaderId', 'tableRowHeader'], + ['titleUiElement', 'titleUIElement']]; + +var intListAttributes = [ + 'characterOffsets', + 'lineBreaks', + 'wordEnds', + 'wordStarts']; + +var nodeRefListAttributes = [ + ['cellIds', 'cells'], + ['controlsIds', 'controls'], + ['describedbyIds', 'describedBy'], + ['flowtoIds', 'flowTo'], + ['labelledbyIds', 'labelledBy'], + ['uniqueCellIds', 'uniqueCells']]; + +var floatAttributes = [ + 'docLoadingProgress', + 'valueForRange', + 'minValueForRange', + 'maxValueForRange', + 'fontSize']; + +var htmlAttributes = [ + ['type', 'inputType']]; + +var publicAttributes = []; + +stringAttributes.forEach(function (attributeName) { + publicAttributes.push(attributeName); + Object.defineProperty(AutomationNodeImpl.prototype, attributeName, { + get: function() { + return GetStringAttribute(this.treeID, this.id, attributeName); + } + }); +}); + +boolAttributes.forEach(function (attributeName) { + publicAttributes.push(attributeName); + Object.defineProperty(AutomationNodeImpl.prototype, attributeName, { + get: function() { + return GetBoolAttribute(this.treeID, this.id, attributeName); + } + }); +}); + +intAttributes.forEach(function (attributeName) { + publicAttributes.push(attributeName); + Object.defineProperty(AutomationNodeImpl.prototype, attributeName, { + get: function() { + return GetIntAttribute(this.treeID, this.id, attributeName); + } + }); +}); + +nodeRefAttributes.forEach(function (params) { + var srcAttributeName = params[0]; + var dstAttributeName = params[1]; + publicAttributes.push(dstAttributeName); + Object.defineProperty(AutomationNodeImpl.prototype, dstAttributeName, { + get: function() { + var id = GetIntAttribute(this.treeID, this.id, srcAttributeName); + if (id) + return this.rootImpl.get(id); + else + return undefined; + } + }); +}); + +intListAttributes.forEach(function (attributeName) { + publicAttributes.push(attributeName); + Object.defineProperty(AutomationNodeImpl.prototype, attributeName, { + get: function() { + return GetIntListAttribute(this.treeID, this.id, attributeName); + } + }); +}); + +nodeRefListAttributes.forEach(function (params) { + var srcAttributeName = params[0]; + var dstAttributeName = params[1]; + publicAttributes.push(dstAttributeName); + Object.defineProperty(AutomationNodeImpl.prototype, dstAttributeName, { + get: function() { + var ids = GetIntListAttribute(this.treeID, this.id, srcAttributeName); + if (!ids) + return undefined; + var result = []; + for (var i = 0; i < ids.length; ++i) { + var node = this.rootImpl.get(ids[i]); + if (node) + result.push(node); + } + return result; + } + }); +}); -var TableCellMixinAttributes = { - tableCellColumnIndex: defaultIntAttribute(), - tableCellColumnSpan: defaultIntAttribute(1), - tableCellRowIndex: defaultIntAttribute(), - tableCellRowSpan: defaultIntAttribute(1) -}; +floatAttributes.forEach(function (attributeName) { + publicAttributes.push(attributeName); + Object.defineProperty(AutomationNodeImpl.prototype, attributeName, { + get: function() { + return GetFloatAttribute(this.treeID, this.id, attributeName); + } + }); +}); -var LiveRegionMixinAttributes = { - containerLiveAtomic: defaultBoolAttribute(), - containerLiveBusy: defaultBoolAttribute(), - containerLiveRelevant: defaultStringAttribute(), - containerLiveStatus: defaultStringAttribute(), -}; +htmlAttributes.forEach(function (params) { + var srcAttributeName = params[0]; + var dstAttributeName = params[1]; + publicAttributes.push(dstAttributeName); + Object.defineProperty(AutomationNodeImpl.prototype, dstAttributeName, { + get: function() { + return GetHtmlAttribute(this.treeID, this.id, srcAttributeName); + } + }); +}); /** * AutomationRootNode. @@ -523,107 +706,88 @@ function AutomationRootNodeImpl(treeID) { this.axNodeDataCache_ = {}; } +AutomationRootNodeImpl.idToAutomationRootNode_ = {}; + +AutomationRootNodeImpl.get = function(treeID) { + var result = AutomationRootNodeImpl.idToAutomationRootNode_[treeID]; + return result || undefined; +} + +AutomationRootNodeImpl.getOrCreate = function(treeID) { + if (AutomationRootNodeImpl.idToAutomationRootNode_[treeID]) + return AutomationRootNodeImpl.idToAutomationRootNode_[treeID]; + var result = new AutomationRootNode(treeID); + AutomationRootNodeImpl.idToAutomationRootNode_[treeID] = result; + return result; +} + +AutomationRootNodeImpl.destroy = function(treeID) { + delete AutomationRootNodeImpl.idToAutomationRootNode_[treeID]; +} + AutomationRootNodeImpl.prototype = { __proto__: AutomationNodeImpl.prototype, + /** + * @type {boolean} + */ isRootNode: true, + + /** + * @type {number} + */ treeID: -1, + /** + * The parent of this node from a different tree. + * @type {?AutomationNode} + * @private + */ + hostNode_: null, + + /** + * A map from id to AutomationNode. + * @type {Object.<number, AutomationNode>} + * @private + */ + axNodeDataCache_: null, + + get id() { + return GetRootID(this.treeID); + }, + get: function(id) { if (id == undefined) return undefined; - return this.axNodeDataCache_[id]; - }, + if (id == this.id) + return this.wrapper; - unserialize: function(update) { - var updateState = { pendingNodes: {}, newNodes: {} }; - var oldRootId = this.id; + var obj = this.axNodeDataCache_[id]; + if (obj) + return obj; - 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; - } + obj = new AutomationNode(this); + privates(obj).impl.treeID = this.treeID; + privates(obj).impl.id = id; + this.axNodeDataCache_[id] = obj; - // Notify tree change observers of new nodes. - // TODO(dmazzoni): Notify tree change observers of changed nodes, - // and handle subtreeCreated and nodeCreated properly. - var observers = automationUtil.treeChangeObservers; - if (observers.length > 0) { - for (var nodeId in updateState.newNodes) { - var node = updateState.newNodes[nodeId]; - var treeChange = - {target: node, type: schema.TreeChangeType.nodeCreated}; - for (var i = 0; i < observers.length; i++) { - try { - observers[i](treeChange); - } catch (e) { - console.error('Error in tree change observer for ' + - treeChange.type + ': ' + e.message + - '\nStack trace: ' + e.stack); - } - } - } - } + return obj; + }, - return true; + remove: function(id) { + delete this.axNodeDataCache_[id]; }, 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; - } + setHostNode(hostNode) { + this.hostNode_ = hostNode; + }, + onAccessibilityEvent: function(eventParams) { var targetNode = this.get(eventParams.targetID); if (targetNode) { var targetNodeImpl = privates(targetNode).impl; @@ -651,374 +815,8 @@ AutomationRootNodeImpl.prototype = { } return toStringInternal(this, 0); }, - - invalidate_: function(node) { - if (!node) - return; - - // Notify tree change observers of the removed node. - var observers = automationUtil.treeChangeObservers; - if (observers.length > 0) { - var treeChange = {target: node, type: schema.TreeChangeType.nodeRemoved}; - for (var i = 0; i < observers.length; i++) { - try { - observers[i](treeChange); - } catch (e) { - console.error('Error in tree change observer for ' + treeChange.type + - ': ' + e.message + '\nStack trace: ' + e.stack); - } - } - } - - 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 || - nodeData.role == schema.RoleType.embeddedObject) { - if (nodeImpl.childTreeID !== nodeData.intAttributes.childTreeId) - nodeImpl.pendingChildFrame = true; - - if (nodeImpl.pendingChildFrame) { - nodeImpl.childTreeID = nodeData.intAttributes.childTreeId; - automationUtil.storeTreeCallback(nodeImpl.childTreeID, function(root) { - nodeImpl.pendingChildFrame = false; - nodeImpl.childTree = root; - privates(root).impl.hostTree = node; - if (root.attributes.docLoadingProgress == 1) - privates(root).impl.dispatchEvent(schema.EventType.loadComplete); - nodeImpl.dispatchEvent(schema.EventType.childrenChanged); - }); - automationInternal.enableFrame(nodeImpl.childTreeID); - } - } - 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 inside a live region, set live region mixins. - var attr = 'containerLiveStatus'; - var spec = LiveRegionMixinAttributes[attr]; - if (this.findAttribute_(attr, spec, nodeData) !== undefined) { - this.mixinAttributes_(nodeImpl, LiveRegionMixinAttributes, nodeData); - } - - // 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.spinButton) { - 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', @@ -1033,7 +831,8 @@ var AutomationNode = utils.expose('AutomationNode', 'removeEventListener', 'domQuerySelector', 'toString' ], - readonly: ['parent', + readonly: publicAttributes.concat( + ['parent', 'firstChild', 'lastChild', 'children', @@ -1043,13 +842,24 @@ var AutomationNode = utils.expose('AutomationNode', 'role', 'state', 'location', - 'attributes', 'indexInParent', - 'root'] }); + 'root']) }); var AutomationRootNode = utils.expose('AutomationRootNode', AutomationRootNodeImpl, { superclass: AutomationNode }); +AutomationRootNode.get = function(treeID) { + return AutomationRootNodeImpl.get(treeID); +} + +AutomationRootNode.getOrCreate = function(treeID) { + return AutomationRootNodeImpl.getOrCreate(treeID); +} + +AutomationRootNode.destroy = function(treeID) { + AutomationRootNodeImpl.destroy(treeID); +} + exports.AutomationNode = AutomationNode; exports.AutomationRootNode = AutomationRootNode; diff --git a/chrome/renderer/resources/extensions/automation_custom_bindings.js b/chrome/renderer/resources/extensions/automation_custom_bindings.js index b2fc5c3b7..59234f2 100644 --- a/chrome/renderer/resources/extensions/automation_custom_bindings.js +++ b/chrome/renderer/resources/extensions/automation_custom_bindings.js @@ -16,6 +16,11 @@ var logging = requireNative('logging'); var nativeAutomationInternal = requireNative('automationInternal'); var GetRoutingID = nativeAutomationInternal.GetRoutingID; var GetSchemaAdditions = nativeAutomationInternal.GetSchemaAdditions; +var DestroyAccessibilityTree = + nativeAutomationInternal.DestroyAccessibilityTree; +var GetIntAttribute = nativeAutomationInternal.GetIntAttribute; +var StartCachingAccessibilityTrees = + nativeAutomationInternal.StartCachingAccessibilityTrees; var schema = GetSchemaAdditions(); /** @@ -24,7 +29,6 @@ var schema = GetSchemaAdditions(); window.automationUtil = function() {}; // TODO(aboxhall): Look into using WeakMap -var idToAutomationRootNode = {}; var idToCallback = {}; var DESKTOP_TREE_ID = 0; @@ -33,7 +37,7 @@ automationUtil.storeTreeCallback = function(id, callback) { if (!callback) return; - var targetTree = idToAutomationRootNode[id]; + 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. @@ -58,6 +62,7 @@ automation.registerCustomHook(function(bindingsAPI) { // TODO(aboxhall, dtseng): Make this return the speced AutomationRootNode obj. apiFunctions.setHandleRequest('getTree', function getTree(tabID, callback) { var routingID = GetRoutingID(); + StartCachingAccessibilityTrees(); // enableTab() ensures the renderer for the active or specified tab has // accessibility enabled, and fetches its ax tree id to use as @@ -79,8 +84,8 @@ automation.registerCustomHook(function(bindingsAPI) { var desktopTree = null; apiFunctions.setHandleRequest('getDesktop', function(callback) { - desktopTree = - idToAutomationRootNode[DESKTOP_TREE_ID]; + StartCachingAccessibilityTrees(); + desktopTree = AutomationRootNode.get(DESKTOP_TREE_ID); if (!desktopTree) { if (DESKTOP_TREE_ID in idToCallback) idToCallback[DESKTOP_TREE_ID].push(callback); @@ -93,8 +98,7 @@ automation.registerCustomHook(function(bindingsAPI) { // scope. automationInternal.enableDesktop(routingID, function() { if (lastError.hasError(chrome)) { - delete idToAutomationRootNode[ - DESKTOP_TREE_ID]; + AutomationRootNode.destroy(DESKTOP_TREE_ID); callback(); return; } @@ -125,19 +129,65 @@ automation.registerCustomHook(function(bindingsAPI) { }); +automationInternal.onTreeChange.addListener(function(treeID, + nodeID, + changeType) { + var tree = AutomationRootNode.get(treeID); + if (!tree) + return; + + var node = privates(tree).impl.get(nodeID); + if (!node) + return; + + if (node.role == 'webView' || node.role == 'embeddedObject') { + // 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. + var childTreeID = GetIntAttribute(treeID, nodeID, 'childTreeId'); + if (!AutomationRootNode.get(childTreeID)) { + automationUtil.storeTreeCallback(childTreeID, function(root) { + privates(root).impl.setHostNode(node); + + if (root.docLoaded) + privates(root).impl.dispatchEvent(schema.EventType.loadComplete); + + privates(node).impl.dispatchEvent(schema.EventType.childrenChanged); + }); + + automationInternal.enableFrame(childTreeID); + } + } + + var treeChange = {target: node, type: changeType}; + + // Make a copy of the observers in case one of these callbacks tries + // to change the list of observers. + var observers = automationUtil.treeChangeObservers.slice(); + for (var i = 0; i < observers.length; i++) { + try { + observers[i](treeChange); + } catch (e) { + logging.WARNING('Error in tree change observer for ' + + treeChange.type + ': ' + e.message + + '\nStack trace: ' + e.stack); + } + } + + if (changeType == schema.TreeChangeType.nodeRemoved) { + privates(tree).impl.remove(nodeID); + } +}); + // Listen to the automationInternal.onAccessibilityEvent event, which is // essentially a proxy for the AccessibilityHostMsg_Events IPC from the // renderer. automationInternal.onAccessibilityEvent.addListener(function(data) { var id = data.treeID; - var targetTree = idToAutomationRootNode[id]; - if (!targetTree) { - // If this is the first time we've gotten data for this tree, it will - // contain all of the tree's data, so create a new tree which will be - // bootstrapped from |data|. - targetTree = new AutomationRootNode(id); - idToAutomationRootNode[id] = targetTree; - } + var targetTree = AutomationRootNode.getOrCreate(id); + if (!privates(targetTree).impl.onAccessibilityEvent(data)) return; @@ -149,7 +199,7 @@ automationInternal.onAccessibilityEvent.addListener(function(data) { // 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.attributes.url && + if (id != DESKTOP_TREE_ID && !targetTree.url && targetTree.children.length == 0) { return; } @@ -158,7 +208,6 @@ automationInternal.onAccessibilityEvent.addListener(function(data) { // 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++) { - console.log('calling getTree() callback'); var callback = idToCallback[id][i]; callback(targetTree); } @@ -166,14 +215,17 @@ automationInternal.onAccessibilityEvent.addListener(function(data) { }); automationInternal.onAccessibilityTreeDestroyed.addListener(function(id) { - var targetTree = idToAutomationRootNode[id]; + // Destroy the AutomationRootNode. + var targetTree = AutomationRootNode.get(id); if (targetTree) { privates(targetTree).impl.destroy(); - delete idToAutomationRootNode[id]; + AutomationRootNode.destroy(id); } else { logging.WARNING('no targetTree to destroy'); } - delete idToAutomationRootNode[id]; + + // Destroy the native cache of the accessibility tree. + DestroyAccessibilityTree(id); }); exports.binding = automation.generate(); diff --git a/chrome/test/data/extensions/api_test/automation/sites/mixins.html b/chrome/test/data/extensions/api_test/automation/sites/attributes.html index c27429d..5b8f63f 100644 --- a/chrome/test/data/extensions/api_test/automation/sites/mixins.html +++ b/chrome/test/data/extensions/api_test/automation/sites/attributes.html @@ -5,12 +5,12 @@ --> <html> <head> -<title>Automation Tests - Mixin attributes</title> +<title>Automation Tests - Attributes</title> </head> <body> - <!-- activedescendant mixin, owns default mixin--> + <!-- activedescendant attribute, owns default attribute--> <input type="text" aria-activedescendant="opt6" aria-readonly="true" - aria-owns="combobox-list" aria-autocomplete="list" role="combobox" + aria-autocomplete="list" role="combobox" id="combobox-edit"> <ul aria-expanded="true" role="listbox" id="combobox-list"> <li role="option" id="opt1">Alabama</li> @@ -22,14 +22,14 @@ <li role="option" id="opt7">Colorado</li> </ul> - <!-- link mixins --> + <!-- link attributes --> <a href="about://blank" id="real-link">Real link</a> <div role="link" id="link-role">ARIA link</div> - <!-- editable text mixins --> - <input type="text" value="Text input" id="text-input"></input> - <textarea id="textarea">Textarea</textarea> - <div tabindex="0" contenteditable role="textbox" id="textbox-role"> + <!-- editable text attributes --> + <input type="text" value="Text input" aria-label="text-input" id="text-input"></input> + <textarea aria-label="textarea">Textarea</textarea> + <div tabindex="0" contenteditable role="textbox" aria-label="textbox-role"> Textbox </div> @@ -37,21 +37,22 @@ document.querySelector('#text-input').setSelectionRange(2, 8); </script> - <!-- range mixins --> - <input type="range" id="range-input" max="5" value="4"></input> + <!-- range attributes --> + <input type="range" aria-label="range-input" max="5" value="4"></input> <div tabindex="0" role="slider" aria-valuemin="1" aria-valuemax="10" - aria-valuenow="7" aria-valuetext="seven stars" id="slider-role"></div> + aria-valuenow="7" aria-valuetext="seven stars" + aria-label="slider-role"></div> <div tabindex="0" role="spinbutton" aria-valuemin="1" aria-valuemax="31" aria-valuenow="14" id="spinbutton-role"></div> <div tabindex="0" role="progressbar" aria-valuemin="0" aria-valuenow="0.9" - aria-valuemax="1.0" id="progressbar-role"></div> + aria-valuemax="1.0" aria-label="progressbar-role"></div> <div tabindex="0" role="scrollbar" aria-valuemin="0" aria-valuenow="0" aria-valuemax="1.0" aria-orientation="vertical" aria-controls="main" id="scrollbar-role"></div> - <div id="main">Content for scrollbar to control</div> + <div id="main" aria-label="main">Content for scrollbar to control</div> - <!-- table and cell mixins --> + <!-- table and cell attributes --> <table id="table" role="grid"> <tr role="row"> <td role="cell">Cell spanning one column</td> diff --git a/chrome/test/data/extensions/api_test/automation/tests/tabs/mixins.html b/chrome/test/data/extensions/api_test/automation/tests/tabs/attributes.html index f60d19c..c2eba0b 100644 --- a/chrome/test/data/extensions/api_test/automation/tests/tabs/mixins.html +++ b/chrome/test/data/extensions/api_test/automation/tests/tabs/attributes.html @@ -4,4 +4,4 @@ * LICENSE file. --> <script src="common.js"></script> -<script src="mixins.js"></script> +<script src="attributes.js"></script> diff --git a/chrome/test/data/extensions/api_test/automation/tests/tabs/mixins.js b/chrome/test/data/extensions/api_test/automation/tests/tabs/attributes.js index b3ae61b..0e0595e 100644 --- a/chrome/test/data/extensions/api_test/automation/tests/tabs/mixins.js +++ b/chrome/test/data/extensions/api_test/automation/tests/tabs/attributes.js @@ -2,45 +2,45 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -var ActiveDescendantMixin = [ 'activedescendant' ]; -var LinkMixins = [ 'url' ]; -var DocumentMixins = [ 'docUrl', - 'docTitle', - 'docLoaded', - 'docLoadingProgress' ]; -var ScrollableMixins = [ 'scrollX', - 'scrollXMin', - 'scrollXMax', - 'scrollY', - 'scrollYMin', - 'scrollYMax' ]; -var EditableTextMixins = [ 'textSelStart', - 'textSelEnd' ]; -var RangeMixins = [ 'valueForRange', - 'minValueForRange', - 'maxValueForRange' ]; -var TableMixins = [ 'tableRowCount', - 'tableColumnCount' ]; -var TableCellMixins = [ 'tableCellColumnIndex', - 'tableCellColumnSpan', - 'tableCellRowIndex', - 'tableCellRowSpan' ]; +var ActiveDescendantAttribute = [ 'activedescendant' ]; +var LinkAttributes = [ 'url' ]; +var DocumentAttributes = [ 'docUrl', + 'docTitle', + 'docLoaded', + 'docLoadingProgress' ]; +var ScrollableAttributes = [ 'scrollX', + 'scrollXMin', + 'scrollXMax', + 'scrollY', + 'scrollYMin', + 'scrollYMax' ]; +var EditableTextAttributes = [ 'textSelStart', + 'textSelEnd' ]; +var RangeAttributes = [ 'valueForRange', + 'minValueForRange', + 'maxValueForRange' ]; +var TableAttributes = [ 'tableRowCount', + 'tableColumnCount' ]; +var TableCellAttributes = [ 'tableCellColumnIndex', + 'tableCellColumnSpan', + 'tableCellRowIndex', + 'tableCellRowSpan' ]; var allTests = [ - function testDocumentAndScrollMixins() { - for (var i = 0; i < DocumentMixins.length; i++) { - var mixinAttribute = DocumentMixins[i]; - assertTrue(mixinAttribute in rootNode, - 'rootNode should have a ' + mixinAttribute + ' attribute'); + function testDocumentAndScrollAttributes() { + for (var i = 0; i < DocumentAttributes.length; i++) { + var attribute = DocumentAttributes[i]; + assertTrue(attribute in rootNode, + 'rootNode should have a ' + attribute + ' attribute'); } - for (var i = 0; i < ScrollableMixins.length; i++) { - var mixinAttribute = ScrollableMixins[i]; - assertTrue(mixinAttribute in rootNode, - 'rootNode should have a ' + mixinAttribute + ' attribute'); + for (var i = 0; i < ScrollableAttributes.length; i++) { + var attribute = ScrollableAttributes[i]; + assertTrue(attribute in rootNode, + 'rootNode should have a ' + attribute + ' attribute'); } assertEq(url, rootNode.docUrl); - assertEq('Automation Tests - Mixin attributes', rootNode.docTitle); + assertEq('Automation Tests - Attributes', rootNode.docTitle); assertEq(true, rootNode.docLoaded); assertEq(1, rootNode.docLoadingProgress); assertEq(0, rootNode.scrollX); @@ -52,22 +52,17 @@ var allTests = [ chrome.test.succeed(); }, - function testActiveDescendantAndOwns() { + function testActiveDescendant() { var combobox = rootNode.find({ role: 'comboBox' }); - - assertTrue('owns' in combobox, 'combobox should have an owns attribute'); - assertEq(1, combobox.owns.length); - var listbox = rootNode.find({ role: 'listBox' }); - assertEq(listbox, combobox.owns[0]); - assertTrue('activedescendant' in combobox, 'combobox should have an activedescendant attribute'); + var listbox = rootNode.find({ role: 'listBox' }); var opt6 = listbox.children[5]; assertEq(opt6, combobox.activedescendant); chrome.test.succeed(); }, - function testLinkMixins() { + function testLinkAttributes() { var links = rootNode.findAll({ role: 'link' }); assertEq(2, links.length); @@ -78,56 +73,56 @@ var allTests = [ var ariaLink = links[1]; assertEq('ARIA link', ariaLink.name); - assertTrue('url' in ariaLink, 'ariaLink should have a url attribute'); - assertEq('', ariaLink.url); + assertTrue('url' in ariaLink, 'ariaLink should have an empty url'); + assertEq(undefined, ariaLink.url); chrome.test.succeed(); }, - function testEditableTextMixins() { + function testEditableTextAttributes() { var textFields = rootNode.findAll({ role: 'textField' }); assertEq(3, textFields.length); - var EditableTextMixins = [ 'textSelStart', 'textSelEnd' ]; + var EditableTextAttributes = [ 'textSelStart', 'textSelEnd' ]; for (var i = 0; i < textFields.length; i++) { var textField = textFields[i]; - var id = textField.attributes.id; - for (var j = 0; j < EditableTextMixins.length; j++) { - var mixinAttribute = EditableTextMixins[j]; - assertTrue(mixinAttribute in textField, - 'textField (' + id + ') should have a ' + mixinAttribute + - ' attribute'); + var description = textField.description; + for (var j = 0; j < EditableTextAttributes.length; j++) { + var attribute = EditableTextAttributes[j]; + assertTrue(attribute in textField, + 'textField (' + description + ') should have a ' + + attribute + ' attribute'); } } var input = textFields[0]; - assertEq('text-input', input.attributes.id); + assertEq('text-input', input.description); assertEq(2, input.textSelStart); assertEq(8, input.textSelEnd); var textArea = textFields[1]; - assertEq('textarea', textArea.attributes.id); - for (var i = 0; i < EditableTextMixins.length; i++) { - var mixinAttribute = EditableTextMixins[i]; - assertTrue(mixinAttribute in textArea, - 'textArea should have a ' + mixinAttribute + ' attribute'); + assertEq('textarea', textArea.description); + for (var i = 0; i < EditableTextAttributes.length; i++) { + var attribute = EditableTextAttributes[i]; + assertTrue(attribute in textArea, + 'textArea should have a ' + attribute + ' attribute'); } assertEq(0, textArea.textSelStart); assertEq(0, textArea.textSelEnd); var ariaTextbox = textFields[2]; - assertEq('textbox-role', ariaTextbox.attributes.id); + assertEq('textbox-role', ariaTextbox.description); assertEq(0, ariaTextbox.textSelStart); assertEq(0, ariaTextbox.textSelEnd); chrome.test.succeed(); }, - function testRangeMixins() { + function testRangeAttributes() { var sliders = rootNode.findAll({ role: 'slider' }); assertEq(2, sliders.length); var spinButtons = rootNode.findAll({ role: 'spinButton' }); assertEq(1, spinButtons.length); var progressIndicators = rootNode.findAll({ role: 'progressIndicator' }); assertEq(1, progressIndicators.length); - assertEq('progressbar-role', progressIndicators[0].attributes.id); + assertEq('progressbar-role', progressIndicators[0].description); var scrollBars = rootNode.findAll({ role: 'scrollBar' }); assertEq(1, scrollBars.length); @@ -136,22 +131,22 @@ var allTests = [ for (var i = 0; i < ranges.length; i++) { var range = ranges[i]; - for (var j = 0; j < RangeMixins.length; j++) { - var mixinAttribute = RangeMixins[j]; - assertTrue(mixinAttribute in range, - range.role + ' (' + range.attributes.id + ') should have a ' - + mixinAttribute + ' attribute'); + for (var j = 0; j < RangeAttributes.length; j++) { + var attribute = RangeAttributes[j]; + assertTrue(attribute in range, + range.role + ' (' + range.description + ') should have a ' + + attribute + ' attribute'); } } var inputRange = sliders[0]; - assertEq('range-input', inputRange.attributes.id); + assertEq('range-input', inputRange.description); assertEq(4, inputRange.valueForRange); assertEq(0, inputRange.minValueForRange); assertEq(5, inputRange.maxValueForRange); var ariaSlider = sliders[1]; - assertEq('slider-role', ariaSlider.attributes.id); + assertEq('slider-role', ariaSlider.description); assertEq(7, ariaSlider.valueForRange); assertEq(1, ariaSlider.minValueForRange); assertEq(10, ariaSlider.maxValueForRange); @@ -172,7 +167,7 @@ var allTests = [ chrome.test.succeed(); }, - function testTableMixins() { + function testTableAttributes() { var table = rootNode.find({ role: 'table' });; assertEq(3, table.tableRowCount); assertEq(3, table.tableColumnCount); @@ -225,22 +220,22 @@ var allTests = [ chrome.test.succeed(); }, - function testNoMixins() { - var div = rootNode.find({ attributes: { id: 'main' } }); + function testNoAttributes() { + var div = rootNode.find({ attributes: { description: 'main' } }); assertTrue(div !== undefined); - var allMixins = [].concat(ActiveDescendantMixin, - LinkMixins, - DocumentMixins, - ScrollableMixins, - EditableTextMixins, - RangeMixins, - TableMixins, - TableCellMixins); - for (var mixinAttr in allMixins) { - assertFalse(mixinAttr in div); + var allAttributes = [].concat(ActiveDescendantAttribute, + LinkAttributes, + DocumentAttributes, + ScrollableAttributes, + EditableTextAttributes, + RangeAttributes, + TableAttributes, + TableCellAttributes); + for (var attributeAttr in allAttributes) { + assertFalse(attributeAttr in div); } chrome.test.succeed(); } ]; -setUpAndRunTests(allTests, 'mixins.html'); +setUpAndRunTests(allTests, 'attributes.html'); diff --git a/chrome/test/data/extensions/api_test/automation/tests/tabs/sanity_check.js b/chrome/test/data/extensions/api_test/automation/tests/tabs/sanity_check.js index 64c4e36..c45bd6e 100644 --- a/chrome/test/data/extensions/api_test/automation/tests/tabs/sanity_check.js +++ b/chrome/test/data/extensions/api_test/automation/tests/tabs/sanity_check.js @@ -5,48 +5,47 @@ // Do not test orientation or hover attributes (similar to exclusions on native // accessibility), since they can be inconsistent depending on the environment. var RemoveUntestedStates = function(state) { - delete state[StateType.horizontal]; - delete state[StateType.hovered]; - delete state[StateType.vertical]; + var result = JSON.parse(JSON.stringify(state)); + delete result[StateType.horizontal]; + delete result[StateType.hovered]; + delete result[StateType.vertical]; + return result; }; var allTests = [ function testSimplePage() { var title = rootNode.docTitle; assertEq('Automation Tests', title); - RemoveUntestedStates(rootNode.state); + + var state = RemoveUntestedStates(rootNode.state); assertEq( - {enabled: true, focusable: true, readOnly: true}, - rootNode.state); + {enabled: true, focusable: true, readOnly: true}, + state); + var children = rootNode.children; assertEq(RoleType.rootWebArea, rootNode.role); assertEq(1, children.length); var body = children[0]; assertEq('body', body.htmlTag); - - RemoveUntestedStates(body.state); - assertEq({enabled: true, readOnly: true}, - body.state); + state = RemoveUntestedStates(body.state); + assertEq({enabled: true, readOnly: true}, state); var contentChildren = body.children; assertEq(3, contentChildren.length); var okButton = contentChildren[0]; assertEq('Ok', okButton.name); - RemoveUntestedStates(okButton.state); - assertEq({enabled: true, focusable: true, readOnly: true}, - okButton.state); + state = RemoveUntestedStates(okButton.state); + assertEq({enabled: true, focusable: true, readOnly: true}, state); var userNameInput = contentChildren[1]; assertEq('Username', userNameInput.description); - RemoveUntestedStates(userNameInput.state); - assertEq({enabled: true, focusable: true}, - userNameInput.state); + state = RemoveUntestedStates(userNameInput.state); + assertEq({enabled: true, focusable: true}, state); var cancelButton = contentChildren[2]; assertEq('Cancel', cancelButton.name); - RemoveUntestedStates(cancelButton.state); - assertEq({enabled: true, focusable: true, readOnly: true}, - cancelButton.state); + state = RemoveUntestedStates(cancelButton.state); + assertEq({enabled: true, focusable: true, readOnly: true}, state); // Traversal. assertEq(undefined, rootNode.parent); diff --git a/chrome/test/data/extensions/api_test/automation/tests/tabs/tree_change.js b/chrome/test/data/extensions/api_test/automation/tests/tabs/tree_change.js index dcb1861b..19a1416 100644 --- a/chrome/test/data/extensions/api_test/automation/tests/tabs/tree_change.js +++ b/chrome/test/data/extensions/api_test/automation/tests/tabs/tree_change.js @@ -5,7 +5,7 @@ var allTests = [ function testTreeChangedObserverForCreatingNode() { chrome.automation.addTreeChangeObserver(function(change) { - if (change.type == "nodeCreated" && change.target.name == "New") { + if (change.type == "subtreeCreated" && change.target.name == "New") { chrome.test.succeed(); } }); diff --git a/chrome/test/data/extensions/api_test/automation/tests/unit/manifest.json b/chrome/test/data/extensions/api_test/automation/tests/unit/manifest.json deleted file mode 100644 index 867927b..0000000 --- a/chrome/test/data/extensions/api_test/automation/tests/unit/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "chrome.automation.unit", - "version": "0.1", - "manifest_version": 2, - "description": "Unittests for chrome.automation.", - "permissions": ["tabs", "http://a.com/"], - "automation": true -} diff --git a/chrome/test/data/extensions/api_test/automation/tests/unit/test.js b/chrome/test/data/extensions/api_test/automation/tests/unit/test.js deleted file mode 100644 index cc788ec..0000000 --- a/chrome/test/data/extensions/api_test/automation/tests/unit/test.js +++ /dev/null @@ -1,508 +0,0 @@ -// 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. - -chrome.test.runWithNativesEnabled(function() { - var moduleSystem = chrome.test.getModuleSystem(window); - window.AutomationRootNode = - moduleSystem.require('automationNode').AutomationRootNode; - window.privates = moduleSystem.privates; - // Unused. - window.automationUtil = function() {}; - window.automationUtil.storeTreeCallback = function() {}; - window.automationUtil.treeChangeObservers = []; -}); - -var assertEq = chrome.test.assertEq; -var assertFalse = chrome.test.assertFalse; -var assertTrue = chrome.test.assertTrue; -var assertIsDef = function(obj) { - assertTrue(obj !== null && obj !== undefined); -} -var assertIsNotDef = function(obj) { - assertTrue(obj === null || obj === undefined); -} -var succeed = chrome.test.succeed; - -var tests = [ - function testAutomationRootNode() { - var root = new AutomationRootNode(); - assertTrue(root.isRootNode); - - succeed(); - }, - - function testAriaRelationshipAttributes() { - var data = { - 'eventType': 'loadComplete', - 'processID': 17, 'routingID': 2, 'targetID': 1, - 'update': { 'nodeIdToClear': 0, 'nodes': [ - { - 'id': 1, 'role': 'rootWebArea', - 'boolAttributes': {'docLoaded': true }, - 'childIds': [5, 6, 7, 8, 9, 10, 11] - }, - { - 'id': 11, 'role': 'div', - 'childIds': [], - 'htmlAttributes': {'id': 'target' } - }, - { - 'id': 5, 'role': 'div', - 'childIds': [], - 'htmlAttributes': {'aria-activedescendant': 'target', - 'id': 'activedescendant'}, - 'intAttributes': {'activedescendantId': 11}, - }, - { - 'id': 6, 'role': 'div', - 'childIds': [], - 'htmlAttributes': {'aria-controls': 'target', 'id': 'controlledBy'}, - 'intlistAttributes': {'controlsIds': [11]}, - }, - { - 'id': 7, 'role': 'div', - 'childIds': [], - 'htmlAttributes': {'aria-describedby': 'target', 'id': 'describedBy'}, - 'intlistAttributes': {'describedbyIds': [11, 6]}, - }, - { - 'id': 8, 'role': 'div', - 'childIds': [], - 'htmlAttributes': {'aria-flowto': 'target', 'id': 'flowTo'}, - 'intlistAttributes': {'flowtoIds': [11]}, - }, - { - 'id': 9, 'role': 'div', - 'childIds': [], - 'htmlAttributes': {'aria-labelledby': 'target', 'id': 'labelledBy'}, - 'intlistAttributes': {'labelledbyIds': [11]} - }, - { - 'id': 10, 'role': 'div', - 'childIds': [], - 'htmlAttributes': {'aria-owns': 'target', 'id': 'owns'}, - 'intlistAttributes': {'ownsIds': [11]}, - } - ]} - } - - var tree = new AutomationRootNode(); - try { - privates(tree).impl.onAccessibilityEvent(data); - } catch (e) { - console.log(e.stack); - } - - var activedescendant = tree.firstChild; - assertIsDef(activedescendant); - assertEq('activedescendant', activedescendant.attributes.id); - assertEq( - 'target', - activedescendant.attributes['aria-activedescendant'].attributes.id); - - assertFalse(activedescendant.activedescendant == null, - 'activedescendant should not be null'); - assertEq( - 'target', - activedescendant.activedescendant.attributes.id); - assertIsNotDef(activedescendant.attributes.activedescendantId); - - var controlledBy = activedescendant.nextSibling; - assertIsDef(controlledBy); - assertEq('controlledBy', controlledBy.attributes.id); - assertEq(1, controlledBy.attributes['aria-controls'].length); - assertEq('target', - controlledBy.attributes['aria-controls'][0].attributes.id); - assertEq(1, controlledBy.controls.length); - assertEq('target', controlledBy.controls[0].attributes.id); - assertIsNotDef(controlledBy.attributes.controlledbyIds); - - var describedBy = controlledBy.nextSibling; - assertIsDef(describedBy); - assertEq('describedBy', describedBy.attributes.id); - assertEq(2, describedBy.attributes['aria-describedby'].length); - assertEq('target', - describedBy.attributes['aria-describedby'][0].attributes.id); - assertEq('controlledBy', - describedBy.attributes['aria-describedby'][1].attributes.id); - assertEq(2, describedBy.describedby.length); - assertEq('target', describedBy.describedby[0].attributes.id); - assertEq('controlledBy', - describedBy.describedby[1].attributes.id); - assertIsNotDef(describedBy.attributes.describedbyIds); - - var flowTo = describedBy.nextSibling; - assertIsDef(flowTo); - assertEq('flowTo', flowTo.attributes.id); - assertEq(1, flowTo.attributes['aria-flowto'].length); - assertEq('target', - flowTo.attributes['aria-flowto'][0].attributes.id); - assertEq(1, flowTo.flowto.length); - assertEq('target', flowTo.flowto[0].attributes.id); - assertIsNotDef(flowTo.attributes.flowtoIds); - - var labelledBy = flowTo.nextSibling; - assertIsDef(labelledBy); - assertEq('labelledBy', labelledBy.attributes.id); - assertEq(1, labelledBy.attributes['aria-labelledby'].length); - assertEq('target', - labelledBy.attributes['aria-labelledby'][0].attributes.id); - assertEq(1, labelledBy.labelledby.length); - assertEq('target', - labelledBy.labelledby[0].attributes.id); - assertIsNotDef(labelledBy.attributes.labelledbyIds); - - var owns = labelledBy.nextSibling; - assertIsDef(owns); - assertEq('owns', owns.attributes.id); - assertEq(1, owns.attributes['aria-owns'].length); - assertEq('target', owns.attributes['aria-owns'][0].attributes.id); - assertEq(1, owns.owns.length); - assertEq('target', owns.owns[0].attributes.id); - assertIsNotDef(owns.attributes.ownsIds); - - succeed(); - }, - - function testCannotSetAttribute() { - var update = - { - 'nodeIdToClear': 0, 'nodes': [ - { - 'id': 1, 'role': 'rootWebArea', - 'boolAttributes': {'docLoaded': true}, - 'childIds': [11] - }, - { - 'id': 11, 'role': 'button', - 'stringAttributes': {'name': 'foo'}, - 'childIds': [] - }] - } - - var tree = new AutomationRootNode(); - assertTrue(privates(tree).impl.unserialize(update)); - var button = tree.firstChild; - assertEq('button', button.role); - assertEq('foo', button.name); - button.name = 'bar'; - assertEq('foo', button.name); - - succeed(); - }, - - function testBadUpdateInvalidChildIds() { - var update = - { - 'nodeIdToClear': 0, 'nodes': [ - { - 'id': 1, 'role': 'rootWebArea', - 'boolAttributes': {'docLoaded': true}, - 'childIds': [5, 6, 7, 8, 9, 10, 11] - }] - } - - var tree = new AutomationRootNode(); - - // This is a bad update because the root references non existent child ids. - assertFalse(privates(tree).impl.unserialize(update)); - - succeed(); - }, - - function testMultipleUpdateNameChanged() { - var update = - { - 'nodeIdToClear': 0, 'nodes': [ - { - 'id': 1, 'role': 'rootWebArea', - 'boolAttributes': {'docLoaded': true}, - 'childIds': [11] - }, - { - 'id': 11, 'role': 'button', - 'stringAttributes': {'name': 'foo'}, - 'childIds': [] - }] - } - - // First, setup the initial tree. - var tree = new AutomationRootNode(); - assertTrue(privates(tree).impl.unserialize(update)); - var button = tree.firstChild; - assertEq('button', button.role); - assertEq('foo', button.name); - - // Now, apply an update that changes the button's name. - // Remove the root since the native serializer stops at the LCA. - update.nodes.splice(0, 1); - update.nodes[0].stringAttributes.name = 'bar'; - - // Make sure the name changes. - assertTrue(privates(tree).impl.unserialize(update)); - assertEq('bar', button.name); - - succeed(); - }, - - function testDocumentAndScrollableMixins() { - var update = { 'nodeIdToClear': 0, 'nodes': [ - { - 'id': 1, 'role': 'rootWebArea', - 'boolAttributes': { 'docLoaded': false }, - 'stringAttributes': { 'docUrl': 'chrome://terms', - 'docTitle': 'Google Chrome Terms of Service' }, - 'intAttributes': { 'scrollY': 583, - 'scrollYMax': 9336 }, - 'floatAttributes': { 'docLoadingProgress': 0.9 }, - 'childIds': [2] - }, - { - 'id': 2, 'role': 'div', - 'childIds': [], - 'htmlAttributes': { 'id': 'child' }, - }, - ] }; - - var tree = new AutomationRootNode(); - assertTrue(privates(tree).impl.unserialize(update)); - assertEq(false, tree.docLoaded); - assertEq('chrome://terms', tree.docUrl); - assertEq('Google Chrome Terms of Service', tree.docTitle); - assertEq('0.9', tree.docLoadingProgress.toPrecision(1)); - assertEq(583, tree.scrollY); - assertEq(9336, tree.scrollYMax); - // Default values will be set for mixin attributes even if not in data. - assertEq(0, tree.scrollYMin); - assertEq(0, tree.scrollX); - assertEq(0, tree.scrollXMin); - assertEq(0, tree.scrollXMax); - - succeed(); - }, - - function testEditableTextMixins() { - var update = { 'nodeIdToClear': 0, 'nodes': [ - { - 'id': 1, 'role': 'rootWebArea', - 'boolAttributes': { 'docLoaded': true }, - 'stringAttributes': { 'docUrl': 'chrome://terms', - 'docTitle': 'Google Chrome Terms of Service' }, - 'intAttributes': { 'scrollY': 583, - 'scrollYMax': 9336 }, - 'childIds': [2, 3] - }, - { - 'id': 2, 'role': 'textField', - 'intAttributes': { 'textSelStart': 10, 'textSelEnd': 20 }, - 'childIds': [] - }, - { - 'id': 3, 'role': 'textField', - 'childIds': [] - }, - - ] }; - - var tree = new AutomationRootNode(); - assertTrue(privates(tree).impl.unserialize(update)); - assertEq(true, tree.docLoaded); - assertFalse('textSelStart' in tree); - assertFalse('textSelEnd' in tree); - var textField = tree.firstChild; - assertEq(10, textField.textSelStart); - assertEq(20, textField.textSelEnd); - var textArea = textField.nextSibling; - assertEq(-1, textArea.textSelStart); - assertEq(-1, textArea.textSelEnd); - - succeed(); - }, - - function testRangeMixins() { - var update = { 'nodeIdToClear': 0, 'nodes': [ - { - 'id': 1, 'role': 'rootWebArea', - 'boolAttributes': { 'docLoaded': true }, - 'stringAttributes': { 'docUrl': 'chrome://terms', - 'docTitle': 'Google Chrome Terms of Service' }, - 'intAttributes': { 'scrollY': 583, - 'scrollYMax': 9336 }, - 'childIds': [2, 3, 4, 5] - }, - { - 'id': 2, 'role': 'progressIndicator', - 'floatAttributes': { 'valueForRange': 1.0, - 'minValueForRange': 0.0, - 'maxValueForRange': 1.0 }, - 'childIds': [] - }, - { - 'id': 3, 'role': 'scrollBar', - 'floatAttributes': { 'valueForRange': 0.3, - 'minValueForRange': 0.0, - 'maxValueForRange': 1.0 }, - 'childIds': [] - }, - { - 'id': 4, 'role': 'slider', - 'floatAttributes': { 'valueForRange': 3.0, - 'minValueForRange': 1.0, - 'maxValueForRange': 5.0 }, - 'childIds': [] - }, - { - 'id': 5, 'role': 'spinButton', - 'floatAttributes': { 'valueForRange': 14.0, - 'minValueForRange': 1.0, - 'maxValueForRange': 31.0 }, - 'childIds': [] - } - ] }; - - var tree = new AutomationRootNode(); - assertTrue(privates(tree).impl.unserialize(update)); - assertEq(true, tree.docLoaded); - assertFalse('valueForRange' in tree); - assertFalse('minValueForRange' in tree); - assertFalse('maxValueForRange' in tree); - - var progressIndicator = tree.firstChild; - assertEq(1.0, progressIndicator.valueForRange); - assertEq(0.0, progressIndicator.minValueForRange); - assertEq(1.0, progressIndicator.maxValueForRange); - - var scrollBar = progressIndicator.nextSibling; - assertEq(0.3, scrollBar.valueForRange); - assertEq(0.0, scrollBar.minValueForRange); - assertEq(1.0, scrollBar.maxValueForRange); - - var slider = scrollBar.nextSibling; - assertEq(3.0, slider.valueForRange); - assertEq(1.0, slider.minValueForRange); - assertEq(5.0, slider.maxValueForRange); - - var spinButton = slider.nextSibling; - assertEq(14.0, spinButton.valueForRange); - assertEq(1.0, spinButton.minValueForRange); - assertEq(31.0, spinButton.maxValueForRange); - - succeed(); - }, - - function testTableMixins() { - var update = { 'nodeIdToClear': 0, 'nodes': [ - { - 'id': 1, 'role': 'rootWebArea', - 'boolAttributes': { 'docLoaded': true }, - 'stringAttributes': { 'docUrl': 'chrome://terms', - 'docTitle': 'Google Chrome Terms of Service' }, - 'intAttributes': { 'scrollY': 583, - 'scrollYMax': 9336 }, - 'childIds': [2] - }, - { - 'id': 2, 'role': 'table', - 'childIds': [3, 6], - 'intAttributes': { tableRowCount: 2, tableColumnCount: 3 } - }, - { - 'id': 3, 'role': 'row', - 'childIds': [4, 5] - }, - { - 'id': 4, 'role': 'cell', - 'intAttributes': { 'tableCellColumnIndex': 0, - 'tableCellColumnSpan': 2, - 'tableCellRowIndex': 0, - 'tableCellRowSpan': 1 }, - 'childIds': [] - }, - { - 'id': 5, 'role': 'cell', - 'intAttributes': { 'tableCellColumnIndex': 2, - 'tableCellColumnSpan': 1, - 'tableCellRowIndex': 0, - 'tableCellRowSpan': 2 }, - 'childIds': [] - }, - { - 'id': 6, 'role': 'row', - 'childIds': [7, 8] - }, - { - 'id': 7, 'role': 'cell', - 'intAttributes': { 'tableCellColumnIndex': 0, - 'tableCellColumnSpan': 1, - 'tableCellRowIndex': 1, - 'tableCellRowSpan': 1 }, - 'childIds': [] - }, - { - 'id': 8, 'role': 'cell', - 'intAttributes': { 'tableCellColumnIndex': 1, - 'tableCellColumnSpan': 1, - 'tableCellRowIndex': 1, - 'tableCellRowSpan': 1 }, - 'childIds': [] - } - ] }; - - var tree = new AutomationRootNode(); - try { - assertTrue(privates(tree).impl.unserialize(update)); - } catch (e) { - console.log(e.stack); - } - var TableMixinAttributes = { - tableRowCount: 0, - tableColumnCount: 0 - }; - for (var attribute in TableMixinAttributes) - assertFalse(attribute in tree); - - var TableCellMixinAttributes = { - tableCellColumnIndex: 0, - tableCellColumnSpan: 1, - tableCellRowIndex: 0, - tableCellRowSpan: 1 - }; - for (var attribute in TableCellMixinAttributes) - assertFalse(attribute in tree); - - var table = tree.firstChild; - assertEq(2, table.tableRowCount); - assertEq(3, table.tableColumnCount); - - var row1 = table.firstChild; - var cell1 = row1.firstChild; - assertEq(0, cell1.tableCellColumnIndex); - assertEq(2, cell1.tableCellColumnSpan); - assertEq(0, cell1.tableCellRowIndex); - assertEq(1, cell1.tableCellRowSpan); - - var cell2 = cell1.nextSibling; - assertEq(2, cell2.tableCellColumnIndex); - assertEq(1, cell2.tableCellColumnSpan); - assertEq(0, cell2.tableCellRowIndex); - assertEq(2, cell2.tableCellRowSpan); - - var row2 = row1.nextSibling; - var cell3 = row2.firstChild; - assertEq(0, cell3.tableCellColumnIndex); - assertEq(1, cell3.tableCellColumnSpan); - assertEq(1, cell3.tableCellRowIndex); - assertEq(1, cell3.tableCellRowSpan); - - var cell4 = cell3.nextSibling; - assertEq(1, cell4.tableCellColumnIndex); - assertEq(1, cell4.tableCellColumnSpan); - assertEq(1, cell4.tableCellRowIndex); - assertEq(1, cell4.tableCellRowSpan); - - succeed(); - } -]; - -chrome.test.runTests(tests); diff --git a/chrome/test/data/extensions/api_test/automation/tests/unit/unit.html b/chrome/test/data/extensions/api_test/automation/tests/unit/unit.html deleted file mode 100644 index 77bad90..0000000 --- a/chrome/test/data/extensions/api_test/automation/tests/unit/unit.html +++ /dev/null @@ -1,6 +0,0 @@ -<!-- - * 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="test.js"></script> diff --git a/content/browser/accessibility/browser_accessibility_manager_unittest.cc b/content/browser/accessibility/browser_accessibility_manager_unittest.cc index d30c258..c9e6d83 100644 --- a/content/browser/accessibility/browser_accessibility_manager_unittest.cc +++ b/content/browser/accessibility/browser_accessibility_manager_unittest.cc @@ -90,13 +90,15 @@ class TestBrowserAccessibilityDelegate return gfx::kNullAcceleratedWidget; } gfx::NativeViewAccessible AccessibilityGetNativeViewAccessible() override { - return NULL; + return nullptr; } BrowserAccessibilityManager* AccessibilityGetChildFrame( int accessibility_node_id) override { - return NULL; + return nullptr; + } + BrowserAccessibility* AccessibilityGetParentFrame() override { + return nullptr; } - BrowserAccessibility* AccessibilityGetParentFrame() override { return NULL; } void AccessibilityGetAllChildFrames( std::vector<BrowserAccessibilityManager*>* child_frames) override {} @@ -141,7 +143,7 @@ TEST(BrowserAccessibilityManagerTest, TestNoLeaks) { BrowserAccessibilityManager* manager = BrowserAccessibilityManager::Create( MakeAXTreeUpdate(root, button, checkbox), - NULL, + nullptr, new CountedBrowserAccessibilityFactory()); ASSERT_EQ(3, CountedBrowserAccessibility::global_obj_count_); @@ -155,7 +157,7 @@ TEST(BrowserAccessibilityManagerTest, TestNoLeaks) { manager = BrowserAccessibilityManager::Create( MakeAXTreeUpdate(root, button, checkbox), - NULL, + nullptr, new CountedBrowserAccessibilityFactory()); ASSERT_EQ(3, CountedBrowserAccessibility::global_obj_count_); @@ -246,7 +248,7 @@ TEST(BrowserAccessibilityManagerTest, TestReuseBrowserAccessibilityObjects) { BrowserAccessibilityManager::Create( MakeAXTreeUpdate(tree1_root, tree1_child1, tree1_child2, tree1_child3), - NULL, + nullptr, new CountedBrowserAccessibilityFactory()); ASSERT_EQ(4, CountedBrowserAccessibility::global_obj_count_); @@ -422,7 +424,7 @@ TEST(BrowserAccessibilityManagerTest, TestReuseBrowserAccessibilityObjects2) { tree1_child1, tree1_grandchild1, tree1_child2, tree1_grandchild2, tree1_child3, tree1_grandchild3), - NULL, + nullptr, new CountedBrowserAccessibilityFactory()); ASSERT_EQ(8, CountedBrowserAccessibility::global_obj_count_); @@ -550,7 +552,7 @@ TEST(BrowserAccessibilityManagerTest, TestMoveChildUp) { BrowserAccessibilityManager* manager = BrowserAccessibilityManager::Create( MakeAXTreeUpdate(tree1_1, tree1_2, tree1_3, tree1_4), - NULL, + nullptr, new CountedBrowserAccessibilityFactory()); ASSERT_EQ(4, CountedBrowserAccessibility::global_obj_count_); @@ -684,7 +686,7 @@ TEST(BrowserAccessibilityManagerTest, BoundsForRange) { scoped_ptr<BrowserAccessibilityManager> manager( BrowserAccessibilityManager::Create( MakeAXTreeUpdate(root, static_text, inline_text1, inline_text2), - NULL, + nullptr, new CountedBrowserAccessibilityFactory())); BrowserAccessibility* root_accessible = manager->GetRoot(); @@ -772,7 +774,7 @@ TEST(BrowserAccessibilityManagerTest, BoundsForRangeBiDi) { scoped_ptr<BrowserAccessibilityManager> manager( BrowserAccessibilityManager::Create( MakeAXTreeUpdate(root, static_text, inline_text1, inline_text2), - NULL, + nullptr, new CountedBrowserAccessibilityFactory())); BrowserAccessibility* root_accessible = manager->GetRoot(); @@ -832,7 +834,7 @@ TEST(BrowserAccessibilityManagerTest, BoundsForRangeScrolledWindow) { scoped_ptr<BrowserAccessibilityManager> manager( BrowserAccessibilityManager::Create( MakeAXTreeUpdate(root, static_text, inline_text), - NULL, + nullptr, new CountedBrowserAccessibilityFactory())); BrowserAccessibility* root_accessible = manager->GetRoot(); @@ -918,7 +920,7 @@ TEST(BrowserAccessibilityManagerTest, MAYBE_BoundsForRangeOnParentElement) { MakeAXTreeUpdate( root, div, static_text1, img, static_text2, inline_text1, inline_text2), - NULL, + nullptr, new CountedBrowserAccessibilityFactory())); BrowserAccessibility* root_accessible = manager->GetRoot(); @@ -965,7 +967,7 @@ TEST(BrowserAccessibilityManagerTest, NextPreviousInTreeOrder) { scoped_ptr<BrowserAccessibilityManager> manager( BrowserAccessibilityManager::Create( MakeAXTreeUpdate(root, node2, node3, node4, node5), - NULL, + nullptr, new CountedBrowserAccessibilityFactory())); BrowserAccessibility* root_accessible = manager->GetRoot(); @@ -975,18 +977,69 @@ TEST(BrowserAccessibilityManagerTest, NextPreviousInTreeOrder) { node3_accessible->PlatformGetChild(0); BrowserAccessibility* node5_accessible = root_accessible->PlatformGetChild(2); - ASSERT_EQ(NULL, manager->NextInTreeOrder(NULL)); + ASSERT_EQ(nullptr, manager->NextInTreeOrder(nullptr)); ASSERT_EQ(node2_accessible, manager->NextInTreeOrder(root_accessible)); ASSERT_EQ(node3_accessible, manager->NextInTreeOrder(node2_accessible)); ASSERT_EQ(node4_accessible, manager->NextInTreeOrder(node3_accessible)); ASSERT_EQ(node5_accessible, manager->NextInTreeOrder(node4_accessible)); - ASSERT_EQ(NULL, manager->NextInTreeOrder(node5_accessible)); + ASSERT_EQ(nullptr, manager->NextInTreeOrder(node5_accessible)); - ASSERT_EQ(NULL, manager->PreviousInTreeOrder(NULL)); + ASSERT_EQ(nullptr, manager->PreviousInTreeOrder(nullptr)); ASSERT_EQ(node4_accessible, manager->PreviousInTreeOrder(node5_accessible)); ASSERT_EQ(node3_accessible, manager->PreviousInTreeOrder(node4_accessible)); ASSERT_EQ(node2_accessible, manager->PreviousInTreeOrder(node3_accessible)); ASSERT_EQ(root_accessible, manager->PreviousInTreeOrder(node2_accessible)); } +TEST(BrowserAccessibilityManagerTest, DeletingFocusedNodeDoesNotCrash) { + // Create a really simple tree with one root node and one focused child. + ui::AXNodeData root; + root.id = 1; + root.role = ui::AX_ROLE_ROOT_WEB_AREA; + root.state = 0; + root.child_ids.push_back(2); + + ui::AXNodeData node2; + node2.id = 2; + node2.state = 1 << ui::AX_STATE_FOCUSED; + + scoped_ptr<BrowserAccessibilityManager> manager( + BrowserAccessibilityManager::Create( + MakeAXTreeUpdate(root, node2), + nullptr, + new CountedBrowserAccessibilityFactory())); + + ASSERT_EQ(1, manager->GetRoot()->GetId()); + ASSERT_EQ(1, manager->GetFocus(manager->GetRoot())->GetId()); + + // Send the focus event for node 2. + std::vector<AccessibilityHostMsg_EventParams> events; + events.push_back(AccessibilityHostMsg_EventParams()); + events[0].update = MakeAXTreeUpdate(node2); + events[0].id = 2; + events[0].event_type = ui::AX_EVENT_FOCUS; + manager->OnAccessibilityEvents(events); + + ASSERT_EQ(1, manager->GetRoot()->GetId()); + ASSERT_EQ(2, manager->GetFocus(manager->GetRoot())->GetId()); + + // Now replace the tree with a new tree consisting of a single root. + ui::AXNodeData root2; + root2.id = 3; + root2.role = ui::AX_ROLE_ROOT_WEB_AREA; + root2.state = 0; + + std::vector<AccessibilityHostMsg_EventParams> events2; + events2.push_back(AccessibilityHostMsg_EventParams()); + events2[0].update = MakeAXTreeUpdate(root2); + events2[0].id = -1; + events2[0].event_type = ui::AX_EVENT_NONE; + manager->OnAccessibilityEvents(events2); + + // Make sure that the focused node was updated to the new root and + // that this doesn't crash. + ASSERT_EQ(3, manager->GetRoot()->GetId()); + ASSERT_EQ(3, manager->GetFocus(manager->GetRoot())->GetId()); +} + } // namespace content diff --git a/extensions/renderer/logging_native_handler.cc b/extensions/renderer/logging_native_handler.cc index 8a913a1..86fbc9c 100644 --- a/extensions/renderer/logging_native_handler.cc +++ b/extensions/renderer/logging_native_handler.cc @@ -2,10 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#include "extensions/renderer/logging_native_handler.h" - #include "base/logging.h" #include "base/strings/stringprintf.h" +#include "extensions/renderer/logging_native_handler.h" +#include "extensions/renderer/script_context.h" namespace extensions { @@ -72,31 +72,7 @@ void LoggingNativeHandler::ParseArgs( *error_message = "Error: " + std::string(*v8::String::Utf8Value(args[1])); } - v8::Local<v8::StackTrace> stack_trace = - v8::StackTrace::CurrentStackTrace(args.GetIsolate(), 10); - if (stack_trace.IsEmpty() || stack_trace->GetFrameCount() <= 0) { - *error_message += "\n <no stack trace>"; - } else { - for (size_t i = 0; i < (size_t)stack_trace->GetFrameCount(); ++i) { - v8::Local<v8::StackFrame> frame = stack_trace->GetFrame(i); - CHECK(!frame.IsEmpty()); - *error_message += base::StringPrintf( - "\n at %s (%s:%d:%d)", - ToStringOrDefault(frame->GetFunctionName(), "<anonymous>").c_str(), - ToStringOrDefault(frame->GetScriptName(), "<anonymous>").c_str(), - frame->GetLineNumber(), - frame->GetColumn()); - } - } -} - -std::string LoggingNativeHandler::ToStringOrDefault( - const v8::Local<v8::String>& v8_string, - const std::string& dflt) { - if (v8_string.IsEmpty()) - return dflt; - std::string ascii_value = *v8::String::Utf8Value(v8_string); - return ascii_value.empty() ? dflt : ascii_value; + *error_message += "\n" + context()->GetStackTraceAsString(); } } // namespace extensions diff --git a/extensions/renderer/logging_native_handler.h b/extensions/renderer/logging_native_handler.h index e9f4f20..ca9938c 100644 --- a/extensions/renderer/logging_native_handler.h +++ b/extensions/renderer/logging_native_handler.h @@ -46,9 +46,6 @@ class LoggingNativeHandler : public ObjectBackedNativeHandler { void ParseArgs(const v8::FunctionCallbackInfo<v8::Value>& args, bool* check_value, std::string* error_message); - - std::string ToStringOrDefault(const v8::Local<v8::String>& v8_string, - const std::string& dflt); }; } // namespace extensions diff --git a/extensions/renderer/script_context.cc b/extensions/renderer/script_context.cc index 58f1b4c..9321468 100644 --- a/extensions/renderer/script_context.cc +++ b/extensions/renderer/script_context.cc @@ -58,6 +58,15 @@ std::string GetContextTypeDescriptionString(Feature::Context context_type) { return std::string(); } +static std::string ToStringOrDefault( + const v8::Local<v8::String>& v8_string, + const std::string& dflt) { + if (v8_string.IsEmpty()) + return dflt; + std::string ascii_value = *v8::String::Utf8Value(v8_string); + return ascii_value.empty() ? dflt : ascii_value; +} + } // namespace // A gin::Runner that delegates to its ScriptContext. @@ -373,6 +382,27 @@ std::string ScriptContext::GetDebugString() const { GetEffectiveContextTypeDescription().c_str()); } +std::string ScriptContext::GetStackTraceAsString() const { + v8::Local<v8::StackTrace> stack_trace = + v8::StackTrace::CurrentStackTrace(isolate(), 10); + if (stack_trace.IsEmpty() || stack_trace->GetFrameCount() <= 0) { + return " <no stack trace>"; + } else { + std::string result; + for (int i = 0; i < stack_trace->GetFrameCount(); ++i) { + v8::Local<v8::StackFrame> frame = stack_trace->GetFrame(i); + CHECK(!frame.IsEmpty()); + result += base::StringPrintf( + "\n at %s (%s:%d:%d)", + ToStringOrDefault(frame->GetFunctionName(), "<anonymous>").c_str(), + ToStringOrDefault(frame->GetScriptName(), "<anonymous>").c_str(), + frame->GetLineNumber(), + frame->GetColumn()); + } + return result; + } +} + ScriptContext::Runner::Runner(ScriptContext* context) : context_(context) { } diff --git a/extensions/renderer/script_context.h b/extensions/renderer/script_context.h index b6e49b9..4a9531a 100644 --- a/extensions/renderer/script_context.h +++ b/extensions/renderer/script_context.h @@ -178,6 +178,9 @@ class ScriptContext : public RequestSender::Source { // Returns a string representation of this ScriptContext, for debugging. std::string GetDebugString() const; + // Gets the current stack trace as a multi-line string to be logged. + std::string GetStackTraceAsString() const; + private: class Runner; diff --git a/ui/accessibility/ax_tree.cc b/ui/accessibility/ax_tree.cc index d07f239..60fa888 100644 --- a/ui/accessibility/ax_tree.cc +++ b/ui/accessibility/ax_tree.cc @@ -204,9 +204,12 @@ bool AXTree::UpdateNode(const AXNodeData& src, // Update the root of the tree if needed. if ((src.role == AX_ROLE_ROOT_WEB_AREA || src.role == AX_ROLE_DESKTOP) && (!root_ || root_->id() != src.id)) { - if (root_) - DestroySubtree(root_, update_state); + // Make sure root_ always points to something valid, even inside + // DestroySubtree. + AXNode* old_root = root_; root_ = node; + if (old_root) + DestroySubtree(old_root, update_state); } return success; @@ -221,11 +224,11 @@ void AXTree::DestroySubtree(AXNode* node, void AXTree::DestroyNodeAndSubtree(AXNode* node, AXTreeUpdateState* update_state) { + if (delegate_) + delegate_->OnNodeWillBeDeleted(this, node); id_map_.erase(node->id()); for (int i = 0; i < node->child_count(); ++i) DestroyNodeAndSubtree(node->ChildAtIndex(i), update_state); - if (delegate_) - delegate_->OnNodeWillBeDeleted(this, node); if (update_state) { update_state->pending_nodes.erase(node); } diff --git a/ui/accessibility/ax_tree_unittest.cc b/ui/accessibility/ax_tree_unittest.cc index 40aeeff..77ef8cd 100644 --- a/ui/accessibility/ax_tree_unittest.cc +++ b/ui/accessibility/ax_tree_unittest.cc @@ -325,8 +325,8 @@ TEST(AXTreeTest, TreeDelegateIsCalled) { EXPECT_TRUE(tree.Unserialize(update)); ASSERT_EQ(2U, fake_delegate.deleted_ids().size()); - EXPECT_EQ(2, fake_delegate.deleted_ids()[0]); - EXPECT_EQ(1, fake_delegate.deleted_ids()[1]); + EXPECT_EQ(1, fake_delegate.deleted_ids()[0]); + EXPECT_EQ(2, fake_delegate.deleted_ids()[1]); ASSERT_EQ(1U, fake_delegate.subtree_deleted_ids().size()); EXPECT_EQ(1, fake_delegate.subtree_deleted_ids()[0]); |