diff options
author | asvitkine <asvitkine@chromium.org> | 2015-07-23 07:37:51 -0700 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2015-07-23 14:38:33 +0000 |
commit | 3cd234a649e3922895a6bb2e4ec786032034bb97 (patch) | |
tree | b92b8518009b8199c15234a7cd8090abfc158d7e | |
parent | 3617707ed710a4e88bcb4a11965d6c0296bf0309 (diff) | |
download | chromium_src-3cd234a649e3922895a6bb2e4ec786032034bb97.zip chromium_src-3cd234a649e3922895a6bb2e4ec786032034bb97.tar.gz chromium_src-3cd234a649e3922895a6bb2e4ec786032034bb97.tar.bz2 |
Revert of Re-land: Reimplement automation API on top of C++-backed AXTree. (patchset #6 id:100001 of https://codereview.chromium.org/1231603009/)
Reason for revert:
Top crasher in today's canary - 74% of browser crashes. Reverting per stability sheriff guidelines (go/stability-sheriff).
Original issue's description:
> 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
>
> Committed: https://crrev.com/1777fbdbd234ddc8e19d4ba3a42cfd3234b8a158
> Cr-Commit-Position: refs/heads/master@{#339929}
TBR=dcheng@chromium.org,aboxhall@chromium.org,dtseng@chromium.org,kalman@chromium.org,dmazzoni@chromium.org
NOPRESUBMIT=true
NOTREECHECKS=true
NOTRY=true
BUG=495323,502311
Review URL: https://codereview.chromium.org/1251723003
Cr-Commit-Position: refs/heads/master@{#340091}
29 files changed, 2152 insertions, 1550 deletions
diff --git a/chrome/browser/extensions/api/automation/automation_apitest.cc b/chrome/browser/extensions/api/automation/automation_apitest.cc index 5f1adcf..8505161 100644 --- a/chrome/browser/extensions/api/automation/automation_apitest.cc +++ b/chrome/browser/extensions/api/automation/automation_apitest.cc @@ -8,15 +8,13 @@ #include "base/single_thread_task_runner.h" #include "base/strings/string_number_conversions.h" #include "base/thread_task_runner_handle.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/api/automation_internal/automation_util.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" @@ -101,6 +99,11 @@ 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")) @@ -200,9 +203,10 @@ IN_PROC_BROWSER_TEST_F(AutomationApiTest, Find) { << message_; } -IN_PROC_BROWSER_TEST_F(AutomationApiTest, Attributes) { +// Flaky. http://crbug.com/467921 +IN_PROC_BROWSER_TEST_F(AutomationApiTest, DISABLED_Mixins) { StartEmbeddedTestServer(); - ASSERT_TRUE(RunExtensionSubtest("automation/tests/tabs", "attributes.html")) + ASSERT_TRUE(RunExtensionSubtest("automation/tests/tabs", "mixins.html")) << message_; } @@ -212,4 +216,328 @@ 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 89ae0c7..c62a3fa 100644 --- a/chrome/browser/extensions/api/automation_internal/automation_event_router.cc +++ b/chrome/browser/extensions/api/automation_internal/automation_event_router.cc @@ -62,8 +62,7 @@ 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)); } } @@ -94,8 +93,7 @@ void AutomationEventRouter::Register( auto iter = std::find_if( listeners_.begin(), listeners_.end(), - [listener_process_id, listener_routing_id]( - const AutomationListener& item) { + [listener_process_id, listener_routing_id](AutomationListener& item) { return (item.process_id == listener_process_id && item.routing_id == listener_routing_id); }); @@ -134,8 +132,8 @@ void AutomationEventRouter::Observe( std::remove_if( listeners_.begin(), listeners_.end(), - [process_id](const AutomationListener& item) { - return item.process_id == process_id; + [process_id](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 58efba5..5f2e8c8 100644 --- a/chrome/browser/extensions/api/automation_internal/automation_internal_api.cc +++ b/chrome/browser/extensions/api/automation_internal/automation_internal_api.cc @@ -12,6 +12,7 @@ #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" @@ -22,16 +23,12 @@ #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" @@ -48,7 +45,6 @@ DEFINE_WEB_CONTENTS_USER_DATA_KEY(extensions::AutomationWebContentsObserver); namespace extensions { namespace { - const int kDesktopTreeID = 0; const char kCannotRequestAutomationOnPage[] = "Cannot request automation tree on url \"*\". " @@ -197,66 +193,24 @@ class AutomationWebContentsObserver void AccessibilityEventReceived( const std::vector<content::AXEventNotificationDetails>& details) override { - 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); - } + automation_util::DispatchAccessibilityEventsToAutomation( + details, browser_context_, + web_contents()->GetContainerBounds().OffsetFromOrigin()); } void RenderFrameDeleted( content::RenderFrameHost* render_frame_host) override { - int tree_id = AXTreeIDRegistry::GetInstance()->GetOrCreateAXTreeID( + automation_util::DispatchTreeDestroyedEventToAutomation( render_frame_host->GetProcess()->GetID(), - render_frame_host->GetRoutingID()); - AXTreeIDRegistry::GetInstance()->RemoveAXTreeID(tree_id); - AutomationEventRouter::GetInstance()->DispatchTreeDestroyedEvent( - tree_id, + render_frame_host->GetRoutingID(), browser_context_); } private: friend class content::WebContentsUserData<AutomationWebContentsObserver>; - explicit AutomationWebContentsObserver(content::WebContents* web_contents) + AutomationWebContentsObserver( + content::WebContents* web_contents) : content::WebContentsObserver(web_contents), browser_context_(web_contents->GetBrowserContext()) {} @@ -291,7 +245,6 @@ 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")); @@ -303,7 +256,6 @@ AutomationInternalEnableTabFunction::Run() { AutomationWebContentsObserver::CreateForWebContents(contents); contents->EnableTreeOnlyAccessibilityMode(); - int ax_tree_id = AXTreeIDRegistry::GetInstance()->GetOrCreateAXTreeID( rfh->GetProcess()->GetID(), rfh->GetRoutingID()); @@ -318,12 +270,11 @@ 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 new file mode 100644 index 0000000..cbd2f9b --- /dev/null +++ b/chrome/browser/extensions/api/automation_internal/automation_util.cc @@ -0,0 +1,213 @@ +// 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, + const std::string& event_name, + scoped_ptr<base::ListValue> args) { + if (context && EventRouter::Get(context)) { + scoped_ptr<Event> event( + new Event(events::UNKNOWN, 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, + 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, + 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 new file mode 100644 index 0000000..218038b --- /dev/null +++ b/chrome/browser/extensions/api/automation_internal/automation_util.h @@ -0,0 +1,41 @@ +// 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 8179d5b..581f90c 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(' + - '$inputType, @input_type_+$inputType, @input_type_text)', + '$type, @input_type_+$type, @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 d1368d5..5bc04e7 100644 --- a/chrome/browser/ui/aura/accessibility/automation_manager_aura.cc +++ b/chrome/browser/ui/aura/accessibility/automation_manager_aura.cc @@ -8,9 +8,8 @@ #include "base/memory/singleton.h" #include "chrome/browser/browser_process.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/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" @@ -20,7 +19,6 @@ #include "ui/views/widget/widget.h" using content::BrowserContext; -using extensions::AutomationEventRouter; // static AutomationManagerAura* AutomationManagerAura::GetInstance() { @@ -66,7 +64,20 @@ 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, @@ -121,28 +132,21 @@ void AutomationManagerAura::ResetSerializer() { void AutomationManagerAura::SendEvent(BrowserContext* context, views::AXAuraObjWrapper* aura_obj, ui::AXEvent event_type) { - 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); - } + 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()); } void AutomationManagerAura::OnNativeFocusChanged(aura::Window* focused_now) { diff --git a/chrome/chrome_browser_extensions.gypi b/chrome/chrome_browser_extensions.gypi index 77f2c89..9602ac5 100644 --- a/chrome/chrome_browser_extensions.gypi +++ b/chrome/chrome_browser_extensions.gypi @@ -138,6 +138,8 @@ '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 b49c3a5..51cdfe7 100644 --- a/chrome/common/extensions/api/automation.idl +++ b/chrome/common/extensions/api/automation.idl @@ -318,6 +318,8 @@ // 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; @@ -368,21 +370,96 @@ // 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; + }; - // - // Link attributes. - // + // Attributes which are mixed in to an AutomationNode if it is a link. + dictionary LinkMixins { + // TODO(aboxhall): Add visited state // The URL that this link will navigate to. DOMString url; + }; - // - // Document attributes. - // - + // Attributes which are mixed in to an AutomationNode if it is a document. + dictionary DocumentMixins { // The URL of this document. DOMString docUrl; @@ -394,22 +471,23 @@ // The proportion (out of 1.0) that this doc has completed loading. double docLoadingProgress; + }; - // - // Scrollable container attributes. - // + // TODO(aboxhall): document ScrollableMixins (e.g. what is scrollXMin? is it + // ever not 0?) + // 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; + }; - // - // Editable text field attributes. - // - + // Attributes which are mixed in to an AutomationNode if it is editable text. + dictionary EditableTextMixins { // The character index of the start of the selection within this editable // text element; -1 if no selection. long textSelStart; @@ -420,11 +498,10 @@ // The input type, like email or number. DOMString textInputType; + }; - // - // Range attributes. - // - + // Attributes which are mixed in to an AutomationNode if it is a range. + dictionary RangeMixins { // The current value for this range. double valueForRange; @@ -433,21 +510,21 @@ // The maximum possible value for this range. double maxValueForRange; + }; - // - // Table attributes. - // + // TODO(aboxhall): live region mixins. + // 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; + }; - // - // Table cell attributes. - // - + // Attributes which are mixed in to an AutomationNode if it is a table cell. + dictionary TableCellMixins { // The zero-based index of the column that this cell is in. long tableCellColumnIndex; @@ -459,75 +536,6 @@ // 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 6f568cf..b290c63 100644 --- a/chrome/common/extensions/api/automation_internal.idl +++ b/chrome/common/extensions/api/automation_internal.idl @@ -6,10 +6,46 @@ // 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. - [nocompile] dictionary AXEventParams { + dictionary AXEventParams { // The tree id of the web contents that this update is for. long treeID; @@ -18,6 +54,10 @@ 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. @@ -93,7 +133,5 @@ 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 632d07d..7f70eb5 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,9 +40,6 @@ v8::Local<v8::Object> ToEnumObject(v8::Isolate* isolate, namespace extensions { -TreeCache::TreeCache() {} -TreeCache::~TreeCache() {} - class AutomationMessageFilter : public IPC::MessageFilter { public: explicit AutomationMessageFilter(AutomationInternalCustomBindings* owner) @@ -50,7 +47,6 @@ class AutomationMessageFilter : public IPC::MessageFilter { removed_(false) { DCHECK(owner); content::RenderThread::Get()->AddFilter(this); - task_runner_ = base::ThreadTaskRunnerHandle::Get(); } void Detach() { @@ -60,15 +56,10 @@ class AutomationMessageFilter : public IPC::MessageFilter { // IPC::MessageFilter bool OnMessageReceived(const IPC::Message& message) override { - 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; + if (owner_) + return owner_->OnMessageReceived(message); + else + return false; } void OnFilterRemoved() override { @@ -76,11 +67,6 @@ class AutomationMessageFilter : public IPC::MessageFilter { } private: - void OnMessageReceivedOnRenderThread(const IPC::Message& message) { - if (owner_) - owner_->OnMessageReceived(message); - } - ~AutomationMessageFilter() override { Remove(); } @@ -94,7 +80,6 @@ private: AutomationInternalCustomBindings* owner_; bool removed_; - scoped_refptr<base::SingleThreadTaskRunner> task_runner_; DISALLOW_COPY_AND_ASSIGN(AutomationMessageFilter); }; @@ -104,46 +89,35 @@ 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. - #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 + 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); } AutomationInternalCustomBindings::~AutomationInternalCustomBindings() { - if (message_filter_) - message_filter_->Detach(); - STLDeleteContainerPairSecondPointers(tree_id_to_tree_cache_map_.begin(), - tree_id_to_tree_cache_map_.end()); + message_filter_->Detach(); } -void AutomationInternalCustomBindings::OnMessageReceived( +bool 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( @@ -162,12 +136,6 @@ 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()); @@ -191,438 +159,9 @@ 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) { - 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); + // TODO(dmazzoni): finish implementing this. } } // namespace extensions diff --git a/chrome/renderer/extensions/automation_internal_custom_bindings.h b/chrome/renderer/extensions/automation_internal_custom_bindings.h index c4f2cd2..1bc8a79 100644 --- a/chrome/renderer/extensions/automation_internal_custom_bindings.h +++ b/chrome/renderer/extensions/automation_internal_custom_bindings.h @@ -6,7 +6,6 @@ #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" @@ -18,27 +17,15 @@ 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, - public ui::AXTreeDelegate { +class AutomationInternalCustomBindings : public ObjectBackedNativeHandler { public: explicit AutomationInternalCustomBindings(ScriptContext* context); ~AutomationInternalCustomBindings() override; - void OnMessageReceived(const IPC::Message& message); + bool OnMessageReceived(const IPC::Message& message); private: // Returns whether this extension has the "interact" permission set (either @@ -52,120 +39,10 @@ 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 e5de712..5dc6b9b 100644 --- a/chrome/renderer/resources/extensions/automation/automation_node.js +++ b/chrome/renderer/resources/extensions/automation/automation_node.js @@ -8,124 +8,6 @@ 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(); @@ -138,12 +20,16 @@ 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 }, @@ -154,51 +40,16 @@ AutomationNodeImpl.prototype = { }, get parent() { - 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); + return this.hostTree || this.rootImpl.get(this.parentID); }, get firstChild() { - 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); + return this.childTree || this.rootImpl.get(this.childIds[0]); }, get lastChild() { - 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); + var childIds = this.childIds; + return this.childTree || this.rootImpl.get(childIds[childIds.length - 1]); }, get children() { @@ -206,28 +57,24 @@ AutomationNodeImpl.prototype = { return [this.childTree]; var children = []; - 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); + for (var i = 0, childID; childID = this.childIds[i]; i++) { + logging.CHECK(this.rootImpl.get(childID)); + children.push(this.rootImpl.get(childID)); } return children; }, get previousSibling() { var parent = this.parent; - var indexInParent = GetIndexInParent(this.treeID, this.id); - if (parent && indexInParent > 0) - return parent.children[indexInParent - 1]; + if (parent && this.indexInParent > 0) + return parent.children[this.indexInParent - 1]; return undefined; }, get nextSibling() { var parent = this.parent; - var indexInParent = GetIndexInParent(this.treeID, this.id); - if (parent && indexInParent < parent.children.length) - return parent.children[indexInParent + 1]; + if (parent && this.indexInParent < parent.children.length) + return parent.children[this.indexInParent + 1]; return undefined; }, @@ -323,20 +170,12 @@ 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=' + parentID + - ' childIds=' + $JSON.stringify(childIDs); + ' parentID=' + impl.parentID + + ' childIds=' + $JSON.stringify(impl.childIds) + + ' attributes=' + $JSON.stringify(this.attributes); }, dispatchEventAtCapturing_: function(event, path) { @@ -380,9 +219,9 @@ AutomationNodeImpl.prototype = { try { listeners[i].callback(event); } catch (e) { - logging.WARNING('Error in event handler for ' + event.type + - ' during phase ' + eventPhase + ': ' + - e.message + '\nStack trace: ' + e.stack); + console.error('Error in event handler for ' + event.type + + 'during phase ' + eventPhase + ': ' + + e.message + '\nStack trace: ' + e.stack); } } }, @@ -473,14 +312,17 @@ 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[attribute] !== attrValue) + if (this.attributesInternal[attribute] !== attrValue) return false; } else if (attrValue instanceof RegExp) { - if (typeof this[attribute] != 'string') + if (typeof this.attributesInternal[attribute] != 'string') return false; - if (!attrValue.test(this[attribute])) + if (!attrValue.test(this.attributesInternal[attribute])) return false; } else { // TODO(aboxhall): handle intlist case. @@ -492,196 +334,171 @@ AutomationNodeImpl.prototype = { } }; -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; - } - }); -}); +// 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 } +}; -floatAttributes.forEach(function (attributeName) { - publicAttributes.push(attributeName); - Object.defineProperty(AutomationNodeImpl.prototype, attributeName, { - get: function() { - return GetFloatAttribute(this.treeID, this.id, attributeName); - } - }); -}); -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); - } - }); -}); +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 TableCellMixinAttributes = { + tableCellColumnIndex: defaultIntAttribute(), + tableCellColumnSpan: defaultIntAttribute(1), + tableCellRowIndex: defaultIntAttribute(), + tableCellRowSpan: defaultIntAttribute(1) +}; + +var LiveRegionMixinAttributes = { + containerLiveAtomic: defaultBoolAttribute(), + containerLiveBusy: defaultBoolAttribute(), + containerLiveRelevant: defaultStringAttribute(), + containerLiveStatus: defaultStringAttribute(), +}; /** * AutomationRootNode. @@ -706,88 +523,107 @@ 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; - if (id == this.id) - return this.wrapper; + return this.axNodeDataCache_[id]; + }, - var obj = this.axNodeDataCache_[id]; - if (obj) - return obj; + unserialize: function(update) { + var updateState = { pendingNodes: {}, newNodes: {} }; + var oldRootId = this.id; - obj = new AutomationNode(this); - privates(obj).impl.treeID = this.treeID; - privates(obj).impl.id = id; - this.axNodeDataCache_[id] = 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; + } + } - return obj; - }, + for (var i = 0; i < update.nodes.length; i++) { + if (!this.updateNode_(update.nodes[i], updateState)) + return false; + } - remove: function(id) { - delete this.axNodeDataCache_[id]; + 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; + } + + // 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 true; }, destroy: function() { - this.dispatchEvent(schema.EventType.destroyed); - }, + if (this.hostTree) + this.hostTree.childTree = undefined; + this.hostTree = undefined; - setHostNode(hostNode) { - this.hostNode_ = hostNode; + this.dispatchEvent(schema.EventType.destroyed); + this.invalidate_(this.wrapper); }, onAccessibilityEvent: function(eventParams) { + if (!this.unserialize(eventParams.update)) { + logging.WARNING('unserialization failed'); + return false; + } + var targetNode = this.get(eventParams.targetID); if (targetNode) { var targetNodeImpl = privates(targetNode).impl; @@ -815,8 +651,374 @@ 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', @@ -831,8 +1033,7 @@ var AutomationNode = utils.expose('AutomationNode', 'removeEventListener', 'domQuerySelector', 'toString' ], - readonly: publicAttributes.concat( - ['parent', + readonly: ['parent', 'firstChild', 'lastChild', 'children', @@ -842,24 +1043,13 @@ 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 59234f2..b2fc5c3b7 100644 --- a/chrome/renderer/resources/extensions/automation_custom_bindings.js +++ b/chrome/renderer/resources/extensions/automation_custom_bindings.js @@ -16,11 +16,6 @@ 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(); /** @@ -29,6 +24,7 @@ var schema = GetSchemaAdditions(); window.automationUtil = function() {}; // TODO(aboxhall): Look into using WeakMap +var idToAutomationRootNode = {}; var idToCallback = {}; var DESKTOP_TREE_ID = 0; @@ -37,7 +33,7 @@ automationUtil.storeTreeCallback = function(id, callback) { if (!callback) return; - var targetTree = AutomationRootNode.get(id); + var targetTree = idToAutomationRootNode[id]; if (!targetTree) { // If we haven't cached the tree, hold the callback until the tree is // populated by the initial onAccessibilityEvent call. @@ -62,7 +58,6 @@ 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 @@ -84,8 +79,8 @@ automation.registerCustomHook(function(bindingsAPI) { var desktopTree = null; apiFunctions.setHandleRequest('getDesktop', function(callback) { - StartCachingAccessibilityTrees(); - desktopTree = AutomationRootNode.get(DESKTOP_TREE_ID); + desktopTree = + idToAutomationRootNode[DESKTOP_TREE_ID]; if (!desktopTree) { if (DESKTOP_TREE_ID in idToCallback) idToCallback[DESKTOP_TREE_ID].push(callback); @@ -98,7 +93,8 @@ automation.registerCustomHook(function(bindingsAPI) { // scope. automationInternal.enableDesktop(routingID, function() { if (lastError.hasError(chrome)) { - AutomationRootNode.destroy(DESKTOP_TREE_ID); + delete idToAutomationRootNode[ + DESKTOP_TREE_ID]; callback(); return; } @@ -129,65 +125,19 @@ 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 = AutomationRootNode.getOrCreate(id); - + 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; + } if (!privates(targetTree).impl.onAccessibilityEvent(data)) return; @@ -199,7 +149,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.url && + if (id != DESKTOP_TREE_ID && !targetTree.attributes.url && targetTree.children.length == 0) { return; } @@ -208,6 +158,7 @@ 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); } @@ -215,17 +166,14 @@ automationInternal.onAccessibilityEvent.addListener(function(data) { }); automationInternal.onAccessibilityTreeDestroyed.addListener(function(id) { - // Destroy the AutomationRootNode. - var targetTree = AutomationRootNode.get(id); + var targetTree = idToAutomationRootNode[id]; if (targetTree) { privates(targetTree).impl.destroy(); - AutomationRootNode.destroy(id); + delete idToAutomationRootNode[id]; } else { logging.WARNING('no targetTree to destroy'); } - - // Destroy the native cache of the accessibility tree. - DestroyAccessibilityTree(id); + delete idToAutomationRootNode[id]; }); exports.binding = automation.generate(); diff --git a/chrome/test/data/extensions/api_test/automation/sites/attributes.html b/chrome/test/data/extensions/api_test/automation/sites/mixins.html index 5b8f63f..c27429d 100644 --- a/chrome/test/data/extensions/api_test/automation/sites/attributes.html +++ b/chrome/test/data/extensions/api_test/automation/sites/mixins.html @@ -5,12 +5,12 @@ --> <html> <head> -<title>Automation Tests - Attributes</title> +<title>Automation Tests - Mixin attributes</title> </head> <body> - <!-- activedescendant attribute, owns default attribute--> + <!-- activedescendant mixin, owns default mixin--> <input type="text" aria-activedescendant="opt6" aria-readonly="true" - aria-autocomplete="list" role="combobox" + aria-owns="combobox-list" 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 attributes --> + <!-- link mixins --> <a href="about://blank" id="real-link">Real link</a> <div role="link" id="link-role">ARIA link</div> - <!-- 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"> + <!-- 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"> Textbox </div> @@ -37,22 +37,21 @@ document.querySelector('#text-input').setSelectionRange(2, 8); </script> - <!-- range attributes --> - <input type="range" aria-label="range-input" max="5" value="4"></input> + <!-- range mixins --> + <input type="range" id="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" - aria-label="slider-role"></div> + aria-valuenow="7" aria-valuetext="seven stars" id="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" aria-label="progressbar-role"></div> + aria-valuemax="1.0" id="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" aria-label="main">Content for scrollbar to control</div> + <div id="main">Content for scrollbar to control</div> - <!-- table and cell attributes --> + <!-- table and cell mixins --> <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/attributes.html b/chrome/test/data/extensions/api_test/automation/tests/tabs/mixins.html index c2eba0b..f60d19c 100644 --- a/chrome/test/data/extensions/api_test/automation/tests/tabs/attributes.html +++ b/chrome/test/data/extensions/api_test/automation/tests/tabs/mixins.html @@ -4,4 +4,4 @@ * LICENSE file. --> <script src="common.js"></script> -<script src="attributes.js"></script> +<script src="mixins.js"></script> diff --git a/chrome/test/data/extensions/api_test/automation/tests/tabs/attributes.js b/chrome/test/data/extensions/api_test/automation/tests/tabs/mixins.js index 0e0595e..b3ae61b 100644 --- a/chrome/test/data/extensions/api_test/automation/tests/tabs/attributes.js +++ b/chrome/test/data/extensions/api_test/automation/tests/tabs/mixins.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 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 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 allTests = [ - function testDocumentAndScrollAttributes() { - for (var i = 0; i < DocumentAttributes.length; i++) { - var attribute = DocumentAttributes[i]; - assertTrue(attribute in rootNode, - 'rootNode should have a ' + attribute + ' attribute'); + function testDocumentAndScrollMixins() { + for (var i = 0; i < DocumentMixins.length; i++) { + var mixinAttribute = DocumentMixins[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'); + for (var i = 0; i < ScrollableMixins.length; i++) { + var mixinAttribute = ScrollableMixins[i]; + assertTrue(mixinAttribute in rootNode, + 'rootNode should have a ' + mixinAttribute + ' attribute'); } assertEq(url, rootNode.docUrl); - assertEq('Automation Tests - Attributes', rootNode.docTitle); + assertEq('Automation Tests - Mixin attributes', rootNode.docTitle); assertEq(true, rootNode.docLoaded); assertEq(1, rootNode.docLoadingProgress); assertEq(0, rootNode.scrollX); @@ -52,17 +52,22 @@ var allTests = [ chrome.test.succeed(); }, - function testActiveDescendant() { + function testActiveDescendantAndOwns() { 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 testLinkAttributes() { + function testLinkMixins() { var links = rootNode.findAll({ role: 'link' }); assertEq(2, links.length); @@ -73,56 +78,56 @@ var allTests = [ var ariaLink = links[1]; assertEq('ARIA link', ariaLink.name); - assertTrue('url' in ariaLink, 'ariaLink should have an empty url'); - assertEq(undefined, ariaLink.url); + assertTrue('url' in ariaLink, 'ariaLink should have a url attribute'); + assertEq('', ariaLink.url); chrome.test.succeed(); }, - function testEditableTextAttributes() { + function testEditableTextMixins() { var textFields = rootNode.findAll({ role: 'textField' }); assertEq(3, textFields.length); - var EditableTextAttributes = [ 'textSelStart', 'textSelEnd' ]; + var EditableTextMixins = [ 'textSelStart', 'textSelEnd' ]; for (var i = 0; i < textFields.length; i++) { var textField = textFields[i]; - 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 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 input = textFields[0]; - assertEq('text-input', input.description); + assertEq('text-input', input.attributes.id); assertEq(2, input.textSelStart); assertEq(8, input.textSelEnd); var textArea = textFields[1]; - 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('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(0, textArea.textSelStart); assertEq(0, textArea.textSelEnd); var ariaTextbox = textFields[2]; - assertEq('textbox-role', ariaTextbox.description); + assertEq('textbox-role', ariaTextbox.attributes.id); assertEq(0, ariaTextbox.textSelStart); assertEq(0, ariaTextbox.textSelEnd); chrome.test.succeed(); }, - function testRangeAttributes() { + function testRangeMixins() { 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].description); + assertEq('progressbar-role', progressIndicators[0].attributes.id); var scrollBars = rootNode.findAll({ role: 'scrollBar' }); assertEq(1, scrollBars.length); @@ -131,22 +136,22 @@ var allTests = [ for (var i = 0; i < ranges.length; i++) { var range = ranges[i]; - 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'); + 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'); } } var inputRange = sliders[0]; - assertEq('range-input', inputRange.description); + assertEq('range-input', inputRange.attributes.id); assertEq(4, inputRange.valueForRange); assertEq(0, inputRange.minValueForRange); assertEq(5, inputRange.maxValueForRange); var ariaSlider = sliders[1]; - assertEq('slider-role', ariaSlider.description); + assertEq('slider-role', ariaSlider.attributes.id); assertEq(7, ariaSlider.valueForRange); assertEq(1, ariaSlider.minValueForRange); assertEq(10, ariaSlider.maxValueForRange); @@ -167,7 +172,7 @@ var allTests = [ chrome.test.succeed(); }, - function testTableAttributes() { + function testTableMixins() { var table = rootNode.find({ role: 'table' });; assertEq(3, table.tableRowCount); assertEq(3, table.tableColumnCount); @@ -220,22 +225,22 @@ var allTests = [ chrome.test.succeed(); }, - function testNoAttributes() { - var div = rootNode.find({ attributes: { description: 'main' } }); + function testNoMixins() { + var div = rootNode.find({ attributes: { id: 'main' } }); assertTrue(div !== undefined); - var allAttributes = [].concat(ActiveDescendantAttribute, - LinkAttributes, - DocumentAttributes, - ScrollableAttributes, - EditableTextAttributes, - RangeAttributes, - TableAttributes, - TableCellAttributes); - for (var attributeAttr in allAttributes) { - assertFalse(attributeAttr in div); + var allMixins = [].concat(ActiveDescendantMixin, + LinkMixins, + DocumentMixins, + ScrollableMixins, + EditableTextMixins, + RangeMixins, + TableMixins, + TableCellMixins); + for (var mixinAttr in allMixins) { + assertFalse(mixinAttr in div); } chrome.test.succeed(); } ]; -setUpAndRunTests(allTests, 'attributes.html'); +setUpAndRunTests(allTests, 'mixins.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 c45bd6e..64c4e36 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,47 +5,48 @@ // 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) { - var result = JSON.parse(JSON.stringify(state)); - delete result[StateType.horizontal]; - delete result[StateType.hovered]; - delete result[StateType.vertical]; - return result; + delete state[StateType.horizontal]; + delete state[StateType.hovered]; + delete state[StateType.vertical]; }; var allTests = [ function testSimplePage() { var title = rootNode.docTitle; assertEq('Automation Tests', title); - - var state = RemoveUntestedStates(rootNode.state); + RemoveUntestedStates(rootNode.state); assertEq( - {enabled: true, focusable: true, readOnly: true}, - state); - + {enabled: true, focusable: true, readOnly: true}, + rootNode.state); var children = rootNode.children; assertEq(RoleType.rootWebArea, rootNode.role); assertEq(1, children.length); var body = children[0]; assertEq('body', body.htmlTag); - state = RemoveUntestedStates(body.state); - assertEq({enabled: true, readOnly: true}, state); + + RemoveUntestedStates(body.state); + assertEq({enabled: true, readOnly: true}, + body.state); var contentChildren = body.children; assertEq(3, contentChildren.length); var okButton = contentChildren[0]; assertEq('Ok', okButton.name); - state = RemoveUntestedStates(okButton.state); - assertEq({enabled: true, focusable: true, readOnly: true}, state); + RemoveUntestedStates(okButton.state); + assertEq({enabled: true, focusable: true, readOnly: true}, + okButton.state); var userNameInput = contentChildren[1]; assertEq('Username', userNameInput.description); - state = RemoveUntestedStates(userNameInput.state); - assertEq({enabled: true, focusable: true}, state); + RemoveUntestedStates(userNameInput.state); + assertEq({enabled: true, focusable: true}, + userNameInput.state); var cancelButton = contentChildren[2]; assertEq('Cancel', cancelButton.name); - state = RemoveUntestedStates(cancelButton.state); - assertEq({enabled: true, focusable: true, readOnly: true}, state); + RemoveUntestedStates(cancelButton.state); + assertEq({enabled: true, focusable: true, readOnly: true}, + cancelButton.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 19a1416..dcb1861b 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 == "subtreeCreated" && change.target.name == "New") { + if (change.type == "nodeCreated" && 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 new file mode 100644 index 0000000..867927b --- /dev/null +++ b/chrome/test/data/extensions/api_test/automation/tests/unit/manifest.json @@ -0,0 +1,8 @@ +{ + "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 new file mode 100644 index 0000000..cc788ec --- /dev/null +++ b/chrome/test/data/extensions/api_test/automation/tests/unit/test.js @@ -0,0 +1,508 @@ +// 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 new file mode 100644 index 0000000..77bad90 --- /dev/null +++ b/chrome/test/data/extensions/api_test/automation/tests/unit/unit.html @@ -0,0 +1,6 @@ +<!-- + * 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 c9e6d83..d30c258 100644 --- a/content/browser/accessibility/browser_accessibility_manager_unittest.cc +++ b/content/browser/accessibility/browser_accessibility_manager_unittest.cc @@ -90,15 +90,13 @@ class TestBrowserAccessibilityDelegate return gfx::kNullAcceleratedWidget; } gfx::NativeViewAccessible AccessibilityGetNativeViewAccessible() override { - return nullptr; + return NULL; } BrowserAccessibilityManager* AccessibilityGetChildFrame( int accessibility_node_id) override { - return nullptr; - } - BrowserAccessibility* AccessibilityGetParentFrame() override { - return nullptr; + return NULL; } + BrowserAccessibility* AccessibilityGetParentFrame() override { return NULL; } void AccessibilityGetAllChildFrames( std::vector<BrowserAccessibilityManager*>* child_frames) override {} @@ -143,7 +141,7 @@ TEST(BrowserAccessibilityManagerTest, TestNoLeaks) { BrowserAccessibilityManager* manager = BrowserAccessibilityManager::Create( MakeAXTreeUpdate(root, button, checkbox), - nullptr, + NULL, new CountedBrowserAccessibilityFactory()); ASSERT_EQ(3, CountedBrowserAccessibility::global_obj_count_); @@ -157,7 +155,7 @@ TEST(BrowserAccessibilityManagerTest, TestNoLeaks) { manager = BrowserAccessibilityManager::Create( MakeAXTreeUpdate(root, button, checkbox), - nullptr, + NULL, new CountedBrowserAccessibilityFactory()); ASSERT_EQ(3, CountedBrowserAccessibility::global_obj_count_); @@ -248,7 +246,7 @@ TEST(BrowserAccessibilityManagerTest, TestReuseBrowserAccessibilityObjects) { BrowserAccessibilityManager::Create( MakeAXTreeUpdate(tree1_root, tree1_child1, tree1_child2, tree1_child3), - nullptr, + NULL, new CountedBrowserAccessibilityFactory()); ASSERT_EQ(4, CountedBrowserAccessibility::global_obj_count_); @@ -424,7 +422,7 @@ TEST(BrowserAccessibilityManagerTest, TestReuseBrowserAccessibilityObjects2) { tree1_child1, tree1_grandchild1, tree1_child2, tree1_grandchild2, tree1_child3, tree1_grandchild3), - nullptr, + NULL, new CountedBrowserAccessibilityFactory()); ASSERT_EQ(8, CountedBrowserAccessibility::global_obj_count_); @@ -552,7 +550,7 @@ TEST(BrowserAccessibilityManagerTest, TestMoveChildUp) { BrowserAccessibilityManager* manager = BrowserAccessibilityManager::Create( MakeAXTreeUpdate(tree1_1, tree1_2, tree1_3, tree1_4), - nullptr, + NULL, new CountedBrowserAccessibilityFactory()); ASSERT_EQ(4, CountedBrowserAccessibility::global_obj_count_); @@ -686,7 +684,7 @@ TEST(BrowserAccessibilityManagerTest, BoundsForRange) { scoped_ptr<BrowserAccessibilityManager> manager( BrowserAccessibilityManager::Create( MakeAXTreeUpdate(root, static_text, inline_text1, inline_text2), - nullptr, + NULL, new CountedBrowserAccessibilityFactory())); BrowserAccessibility* root_accessible = manager->GetRoot(); @@ -774,7 +772,7 @@ TEST(BrowserAccessibilityManagerTest, BoundsForRangeBiDi) { scoped_ptr<BrowserAccessibilityManager> manager( BrowserAccessibilityManager::Create( MakeAXTreeUpdate(root, static_text, inline_text1, inline_text2), - nullptr, + NULL, new CountedBrowserAccessibilityFactory())); BrowserAccessibility* root_accessible = manager->GetRoot(); @@ -834,7 +832,7 @@ TEST(BrowserAccessibilityManagerTest, BoundsForRangeScrolledWindow) { scoped_ptr<BrowserAccessibilityManager> manager( BrowserAccessibilityManager::Create( MakeAXTreeUpdate(root, static_text, inline_text), - nullptr, + NULL, new CountedBrowserAccessibilityFactory())); BrowserAccessibility* root_accessible = manager->GetRoot(); @@ -920,7 +918,7 @@ TEST(BrowserAccessibilityManagerTest, MAYBE_BoundsForRangeOnParentElement) { MakeAXTreeUpdate( root, div, static_text1, img, static_text2, inline_text1, inline_text2), - nullptr, + NULL, new CountedBrowserAccessibilityFactory())); BrowserAccessibility* root_accessible = manager->GetRoot(); @@ -967,7 +965,7 @@ TEST(BrowserAccessibilityManagerTest, NextPreviousInTreeOrder) { scoped_ptr<BrowserAccessibilityManager> manager( BrowserAccessibilityManager::Create( MakeAXTreeUpdate(root, node2, node3, node4, node5), - nullptr, + NULL, new CountedBrowserAccessibilityFactory())); BrowserAccessibility* root_accessible = manager->GetRoot(); @@ -977,69 +975,18 @@ TEST(BrowserAccessibilityManagerTest, NextPreviousInTreeOrder) { node3_accessible->PlatformGetChild(0); BrowserAccessibility* node5_accessible = root_accessible->PlatformGetChild(2); - ASSERT_EQ(nullptr, manager->NextInTreeOrder(nullptr)); + ASSERT_EQ(NULL, manager->NextInTreeOrder(NULL)); 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(nullptr, manager->NextInTreeOrder(node5_accessible)); + ASSERT_EQ(NULL, manager->NextInTreeOrder(node5_accessible)); - ASSERT_EQ(nullptr, manager->PreviousInTreeOrder(nullptr)); + ASSERT_EQ(NULL, manager->PreviousInTreeOrder(NULL)); 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 86fbc9c..8a913a1 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,7 +72,31 @@ void LoggingNativeHandler::ParseArgs( *error_message = "Error: " + std::string(*v8::String::Utf8Value(args[1])); } - *error_message += "\n" + context()->GetStackTraceAsString(); + 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; } } // namespace extensions diff --git a/extensions/renderer/logging_native_handler.h b/extensions/renderer/logging_native_handler.h index ca9938c..e9f4f20 100644 --- a/extensions/renderer/logging_native_handler.h +++ b/extensions/renderer/logging_native_handler.h @@ -46,6 +46,9 @@ 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 9321468..58f1b4c 100644 --- a/extensions/renderer/script_context.cc +++ b/extensions/renderer/script_context.cc @@ -58,15 +58,6 @@ 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. @@ -382,27 +373,6 @@ 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 4a9531a..b6e49b9 100644 --- a/extensions/renderer/script_context.h +++ b/extensions/renderer/script_context.h @@ -178,9 +178,6 @@ 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 60fa888..d07f239 100644 --- a/ui/accessibility/ax_tree.cc +++ b/ui/accessibility/ax_tree.cc @@ -204,12 +204,9 @@ 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)) { - // Make sure root_ always points to something valid, even inside - // DestroySubtree. - AXNode* old_root = root_; + if (root_) + DestroySubtree(root_, update_state); root_ = node; - if (old_root) - DestroySubtree(old_root, update_state); } return success; @@ -224,11 +221,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 77ef8cd..40aeeff 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(1, fake_delegate.deleted_ids()[0]); - EXPECT_EQ(2, fake_delegate.deleted_ids()[1]); + EXPECT_EQ(2, fake_delegate.deleted_ids()[0]); + EXPECT_EQ(1, fake_delegate.deleted_ids()[1]); ASSERT_EQ(1U, fake_delegate.subtree_deleted_ids().size()); EXPECT_EQ(1, fake_delegate.subtree_deleted_ids()[0]); |