// 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 "base/files/file_path.h" #include "base/path_service.h" #include "base/strings/string_number_conversions.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/test/base/ui_test_utils.h" #include "content/public/browser/ax_event_notification_details.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 "extensions/test/extension_test_message_listener.h" #include "net/dns/mock_host_resolver.h" #include "net/test/embedded_test_server/embedded_test_server.h" #include "testing/gtest/include/gtest/gtest.h" #include "ui/accessibility/ax_node.h" #include "ui/accessibility/ax_serializable_tree.h" #include "ui/accessibility/ax_tree.h" #include "ui/accessibility/ax_tree_serializer.h" #include "ui/accessibility/tree_generator.h" #if defined(OS_CHROMEOS) #include "ash/accelerators/accelerator_controller.h" #include "ash/shell.h" #include "chrome/browser/ui/aura/accessibility/automation_manager_aura.h" #endif namespace extensions { namespace { static const char kDomain[] = "a.com"; static const char kSitesDir[] = "automation/sites"; static const char kGotTree[] = "got_tree"; } // anonymous namespace class AutomationApiTest : public ExtensionApiTest { protected: GURL GetURLForPath(const std::string& host, const std::string& path) { std::string port = base::IntToString(embedded_test_server()->port()); GURL::Replacements replacements; replacements.SetHostStr(host); replacements.SetPortStr(port); GURL url = embedded_test_server()->GetURL(path).ReplaceComponents(replacements); return url; } void StartEmbeddedTestServer() { base::FilePath test_data; ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &test_data)); embedded_test_server()->ServeFilesFromDirectory( test_data.AppendASCII("extensions/api_test") .AppendASCII(kSitesDir)); ASSERT_TRUE(ExtensionApiTest::StartEmbeddedTestServer()); host_resolver()->AddRule("*", embedded_test_server()->base_url().host()); } public: virtual void SetUpInProcessBrowserTestFixture() override { ExtensionApiTest::SetUpInProcessBrowserTestFixture(); } }; IN_PROC_BROWSER_TEST_F(AutomationApiTest, TestRendererAccessibilityEnabled) { StartEmbeddedTestServer(); const GURL url = GetURLForPath(kDomain, "/index.html"); ui_test_utils::NavigateToURL(browser(), url); ASSERT_EQ(1, browser()->tab_strip_model()->count()); content::WebContents* const tab = browser()->tab_strip_model()->GetWebContentsAt(0); ASSERT_FALSE(tab->IsFullAccessibilityModeForTesting()); ASSERT_FALSE(tab->IsTreeOnlyAccessibilityModeForTesting()); base::FilePath extension_path = test_data_dir_.AppendASCII("automation/tests/basic"); ExtensionTestMessageListener got_tree(kGotTree, false /* no reply */); LoadExtension(extension_path); ASSERT_TRUE(got_tree.WaitUntilSatisfied()); ASSERT_FALSE(tab->IsFullAccessibilityModeForTesting()); ASSERT_TRUE(tab->IsTreeOnlyAccessibilityModeForTesting()); } IN_PROC_BROWSER_TEST_F(AutomationApiTest, SanityCheck) { StartEmbeddedTestServer(); ASSERT_TRUE(RunExtensionSubtest("automation/tests/tabs", "sanity_check.html")) << 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")) << message_; } IN_PROC_BROWSER_TEST_F(AutomationApiTest, Events) { StartEmbeddedTestServer(); ASSERT_TRUE(RunExtensionSubtest("automation/tests/tabs", "events.html")) << message_; } IN_PROC_BROWSER_TEST_F(AutomationApiTest, Actions) { StartEmbeddedTestServer(); ASSERT_TRUE(RunExtensionSubtest("automation/tests/tabs", "actions.html")) << message_; } IN_PROC_BROWSER_TEST_F(AutomationApiTest, Location) { StartEmbeddedTestServer(); ASSERT_TRUE(RunExtensionSubtest("automation/tests/tabs", "location.html")) << message_; } IN_PROC_BROWSER_TEST_F(AutomationApiTest, TabsAutomationBooleanPermissions) { StartEmbeddedTestServer(); ASSERT_TRUE(RunExtensionSubtest( "automation/tests/tabs_automation_boolean", "permissions.html")) << message_; } IN_PROC_BROWSER_TEST_F(AutomationApiTest, TabsAutomationBooleanActions) { StartEmbeddedTestServer(); ASSERT_TRUE(RunExtensionSubtest( "automation/tests/tabs_automation_boolean", "actions.html")) << message_; } IN_PROC_BROWSER_TEST_F(AutomationApiTest, TabsAutomationHostsPermissions) { StartEmbeddedTestServer(); ASSERT_TRUE(RunExtensionSubtest( "automation/tests/tabs_automation_hosts", "permissions.html")) << message_; } #if defined(USE_AURA) IN_PROC_BROWSER_TEST_F(AutomationApiTest, Desktop) { ASSERT_TRUE(RunExtensionSubtest("automation/tests/desktop", "desktop.html")) << message_; } IN_PROC_BROWSER_TEST_F(AutomationApiTest, DesktopNotRequested) { ASSERT_TRUE(RunExtensionSubtest("automation/tests/tabs", "desktop_not_requested.html")) << message_; } #if defined(OS_CHROMEOS) IN_PROC_BROWSER_TEST_F(AutomationApiTest, DesktopActions) { AutomationManagerAura::GetInstance()->Enable(browser()->profile()); // Trigger the shelf subtree to be computed. ash::Shell::GetInstance()->accelerator_controller()->PerformActionIfEnabled( ash::FOCUS_SHELF); ASSERT_TRUE(RunExtensionSubtest("automation/tests/desktop", "actions.html")) << message_; } IN_PROC_BROWSER_TEST_F(AutomationApiTest, DesktopLoadTabs) { ASSERT_TRUE(RunExtensionSubtest("automation/tests/desktop", "load_tabs.html")) << message_; } #endif // defined(OS_CHROMEOS) #else // !defined(USE_AURA) IN_PROC_BROWSER_TEST_F(AutomationApiTest, DesktopNotSupported) { ASSERT_TRUE(RunExtensionSubtest("automation/tests/desktop", "desktop_not_supported.html")) << message_; } #endif // defined(USE_AURA) IN_PROC_BROWSER_TEST_F(AutomationApiTest, CloseTab) { StartEmbeddedTestServer(); ASSERT_TRUE(RunExtensionSubtest("automation/tests/tabs", "close_tab.html")) << message_; } IN_PROC_BROWSER_TEST_F(AutomationApiTest, QuerySelector) { StartEmbeddedTestServer(); ASSERT_TRUE( RunExtensionSubtest("automation/tests/tabs", "queryselector.html")) << message_; } IN_PROC_BROWSER_TEST_F(AutomationApiTest, Find) { StartEmbeddedTestServer(); ASSERT_TRUE(RunExtensionSubtest("automation/tests/tabs", "find.html")) << message_; } // Flaky on Linux and Windows. http://crbug.com/467921 #if defined(OS_LINUX) || defined(OS_WIN) #define MAYBE_Mixins DISABLED_Mixins #else #define MAYBE_Mixins Mixins #endif IN_PROC_BROWSER_TEST_F(AutomationApiTest, Mixins) { StartEmbeddedTestServer(); ASSERT_TRUE(RunExtensionSubtest("automation/tests/tabs", "mixins.html")) << message_; } IN_PROC_BROWSER_TEST_F(AutomationApiTest, TreeChange) { StartEmbeddedTestServer(); ASSERT_TRUE(RunExtensionSubtest("automation/tests/tabs", "tree_change.html")) << message_; } static const int kPid = 1; static const int kTab0Rid = 1; static const int kTab1Rid = 2; using content::BrowserContext; typedef ui::AXTreeSerializer TreeSerializer; typedef ui::AXTreeSource 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, kPid, routing_id); std::vector 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 tree0, tree1; scoped_ptr tree0_source, tree1_source; scoped_ptr 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::Create(*args_)); EXTENSION_FUNCTION_VALIDATE(params.get()); if (!params->tab_id.get()) return RespondNow(Error("tab_id not specified")); int tab_id = *params->tab_id; if (tab_id == 0) { // tab 0 <--> tree0 base::MessageLoop::current()->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::MessageLoop::current()->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