chromium/chrome/browser/extensions/api/automation/automation_apitest.cc

// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/json/json_reader.h"
#include "base/location.h"
#include "base/path_service.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/test/trace_event_analyzer.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.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/pref_names.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/tracing_controller.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_test.h"
#include "extensions/browser/api/automation_internal/automation_event_router.h"
#include "extensions/common/api/automation_internal.h"
#include "extensions/common/switches.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/test_extension_dir.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 "third_party/blink/public/common/features.h"
#include "ui/accessibility/accessibility_switches.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/ax_updates_and_events.h"
#include "ui/accessibility/tree_generator.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/display/display_switches.h"

#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "ash/public/cpp/accelerators.h"
#include "ash/public/cpp/test/shell_test_api.h"
#include "chrome/browser/ui/aura/accessibility/automation_manager_aura.h"
#include "ui/accessibility/ax_action_handler_registry.h"
#include "ui/display/manager/display_manager.h"
#include "ui/display/screen.h"
#include "ui/display/test/display_manager_test_api.h"  // nogncheck
#endif

namespace extensions {

namespace {

constexpr char kManifestStub[] =;

constexpr char kPersistentBackground[] =;
constexpr char kServiceWorkerBackground[] =;
constexpr char kPermissionsDefault[] =;

#if BUILDFLAG(IS_CHROMEOS) || !defined(USE_AURA)

constexpr char kPermissionsWindows[] = R"(["windows"])";

#endif

static constexpr char kCommonScript[] =;  // kCommonScript

}  // namespace

ContextType;

class AutomationApiTest : public ExtensionApiTest {};

class AutomationApiTestWithContextType
    : public AutomationApiTest,
      public testing::WithParamInterface<ContextType> {};

INSTANTIATE_TEST_SUITE_P();
INSTANTIATE_TEST_SUITE_P();

// Canvas tests rely on the harness producing pixel output in order to read back
// pixels from a canvas element. So we have to override the setup function.
class AutomationApiCanvasTest : public AutomationApiTestWithContextType {};

#if defined(USE_AURA)

namespace {
static const char kDomain[] =;
static const char kGotTree[] =;
}  // anonymous namespace

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType,
                       TestRendererAccessibilityEnabled) {}

IN_PROC_BROWSER_TEST_F(AutomationApiTest, ServiceWorker) {}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, SanityCheck) {}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, ImageLabels) {}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, Events) {}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, Actions) {}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, Location) {}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, Location2) {}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, BoundsForRange) {}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, LineStartOffsets) {}

INSTANTIATE_TEST_SUITE_P();
INSTANTIATE_TEST_SUITE_P();

// Flaky on Mac: crbug.com/1338036
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
#define MAYBE_ImageData
#else
#define MAYBE_ImageData
#endif
IN_PROC_BROWSER_TEST_P(AutomationApiCanvasTest, MAYBE_ImageData) {}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, TableProperties) {}

// Flaky on Mac and Windows: crbug.com/1235249
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
#define MAYBE_CloseTab
#else
#define MAYBE_CloseTab
#endif
IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, MAYBE_CloseTab) {}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, Find) {}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, Attributes) {}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, ReverseRelations) {}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, TreeChange) {}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, TreeChangeIndirect) {}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, DocumentSelection) {}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, HitTest) {}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, WordBoundaries) {}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, SentenceBoundaries) {}

class AutomationApiTestWithLanguageDetection
    : public AutomationApiTestWithContextType {};

INSTANTIATE_TEST_SUITE_P();
INSTANTIATE_TEST_SUITE_P();

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithLanguageDetection,
                       DetectedLanguage) {}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType,
                       IgnoredNodesNotReturned) {}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, ForceLayout) {}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, Intents) {}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, EnumValidity) {}

#endif  // defined(USE_AURA)

#if !defined(USE_AURA)
IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, DesktopNotSupported) {
  ASSERT_TRUE(CreateExtensionAndRunTest("desktop/desktop_not_supported.js",
                                        kPermissionsWindows))
      << message_;
}
#endif  // !defined(USE_AURA)

#if BUILDFLAG(IS_CHROMEOS_ASH)
class AutomationApiFencedFrameTest : public AutomationApiTest {
 protected:
  AutomationApiFencedFrameTest() {
    feature_list_.InitWithFeaturesAndParameters(
        /*enabled_features=*/{{blink::features::kFencedFrames, {}},
                              {features::kPrivacySandboxAdsAPIsOverride, {}},
                              {blink::features::kFencedFramesAPIChanges, {}},
                              {blink::features::kFencedFramesDefaultMode, {}}},
        /*disabled_features=*/{features::kSpareRendererForSitePerProcess});
  }

  ~AutomationApiFencedFrameTest() override = default;

 public:
  base::test::ScopedFeatureList feature_list_;
};

IN_PROC_BROWSER_TEST_F(AutomationApiFencedFrameTest, DesktopFindInFencedframe) {
  StartEmbeddedTestServer();
  ASSERT_TRUE(RunExtensionTest("automation/tests/desktop/fencedframe",
                               {.extension_url = "focus_fencedframe.html"}))
      << message_;
}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, Desktop) {
  ASSERT_TRUE(
      CreateExtensionAndRunTest("desktop/desktop.js", kPermissionsWindows))
      << message_;
}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, DesktopInitialFocus) {
  ASSERT_TRUE(CreateExtensionAndRunTest("desktop/initial_focus.js",
                                        kPermissionsWindows))
      << message_;
}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, DesktopFocusWeb) {
  ASSERT_TRUE(
      CreateExtensionAndRunTest("desktop/focus_web.js", kPermissionsWindows))
      << message_;
}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, DesktopFocusIframe) {
  StartEmbeddedTestServer();
  ASSERT_TRUE(
      CreateExtensionAndRunTest("desktop/focus_iframe.js", kPermissionsWindows))
      << message_;
}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, DesktopHitTestIframe) {
  StartEmbeddedTestServer();
  ASSERT_TRUE(CreateExtensionAndRunTest("desktop/hit_test_iframe.js",
                                        kPermissionsWindows))
      << message_;
}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, DesktopFocusViews) {
  AutomationManagerAura::GetInstance()->Enable();
  // Trigger the shelf subtree to be computed.
  ash::AcceleratorController::Get()->PerformActionIfEnabled(
      ash::AcceleratorAction::kFocusShelf, {});

  ASSERT_TRUE(
      CreateExtensionAndRunTest("desktop/focus_views.js", kPermissionsWindows))
      << message_;
}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType,
                       DesktopGetNextTextMatch) {
  StartEmbeddedTestServer();
  ASSERT_TRUE(CreateExtensionAndRunTest("desktop/get_next_text_match.js",
                                        kPermissionsWindows))
      << message_;
}

IN_PROC_BROWSER_TEST_F(AutomationApiTest, LocationInWebView) {
  StartEmbeddedTestServer();
  ASSERT_TRUE(RunExtensionTest("automation/tests/webview",
                               {.launch_as_platform_app = true}))
      << message_;
}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, DesktopActions) {
  AutomationManagerAura::GetInstance()->Enable();
  // Trigger the shelf subtree to be computed.
  ash::AcceleratorController::Get()->PerformActionIfEnabled(
      ash::AcceleratorAction::kFocusShelf, {});

  ASSERT_TRUE(
      CreateExtensionAndRunTest("desktop/actions.js", kPermissionsWindows))
      << message_;
}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType,
                       DesktopHitTestOneDisplay) {
  ASSERT_TRUE(
      CreateExtensionAndRunTest("desktop/hit_test.js", kPermissionsWindows))
      << message_;
}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType,
                       DesktopHitTestPrimaryDisplay) {
  ash::ShellTestApi shell_test_api;
  // Create two displays, both 800x750px, next to each other. The primary
  // display has top left corner at (0, 0), and the secondary display has
  // top left corner at (801, 0).
  display::test::DisplayManagerTestApi(shell_test_api.display_manager())
      .UpdateDisplay("800x750,801+0-800x750");
  // Ensure it worked. By default InProcessBrowserTest uses just one display.
  ASSERT_EQ(2u, shell_test_api.display_manager()->GetNumDisplays());
  display::test::DisplayManagerTestApi display_manager_test_api(
      shell_test_api.display_manager());
  // The browser will open in the primary display.
  ASSERT_TRUE(
      CreateExtensionAndRunTest("desktop/hit_test.js", kPermissionsWindows))
      << message_;
}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType,
                       DesktopHitTestSecondaryDisplay) {
  ash::ShellTestApi shell_test_api;
  // Create two displays, both 800x750px, next to each other. The primary
  // display has top left corner at (0, 0), and the secondary display has
  // top left corner at (801, 0).
  display::test::DisplayManagerTestApi(shell_test_api.display_manager())
      .UpdateDisplay("800x750,801+0-800x750");
  // Ensure it worked. By default InProcessBrowserTest uses just one display.
  ASSERT_EQ(2u, shell_test_api.display_manager()->GetNumDisplays());
  display::test::DisplayManagerTestApi display_manager_test_api(
      shell_test_api.display_manager());

  display::Screen* screen = display::Screen::GetScreen();
  int64_t display2 = display_manager_test_api.GetSecondaryDisplay().id();
  screen->SetDisplayForNewWindows(display2);
  // Run the test in the browser in the non-primary display.
  // Open a browser on the secondary display, which is default for new windows.
  CreateBrowser(browser()->profile());
  // Close the browser which was already opened on the primary display.
  CloseBrowserSynchronously(browser());
  // Sets browser() to return the one created above, instead of the one which
  // was closed.
  SelectFirstBrowser();
  // The test will run in browser().
  ASSERT_TRUE(
      CreateExtensionAndRunTest("desktop/hit_test.js", kPermissionsWindows))
      << message_;
}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, DesktopLoadTabs) {
  ASSERT_TRUE(
      CreateExtensionAndRunTest("desktop/load_tabs.js", kPermissionsWindows))
      << message_;
}

class AutomationApiTestWithDeviceScaleFactor
    : public AutomationApiTestWithContextType {
 protected:
  void SetUpCommandLine(base::CommandLine* command_line) override {
    AutomationApiTest::SetUpCommandLine(command_line);
    command_line->AppendSwitchASCII(::switches::kForceDeviceScaleFactor, "2.0");
  }
};

INSTANTIATE_TEST_SUITE_P(PersistentBackground,
                         AutomationApiTestWithDeviceScaleFactor,
                         ::testing::Values(ContextType::kPersistentBackground));
INSTANTIATE_TEST_SUITE_P(ServiceWorker,
                         AutomationApiTestWithDeviceScaleFactor,
                         ::testing::Values(ContextType::kServiceWorker));

// Platform apps don't support service worker contexts.
using AutomationApiPlatformAppTestWithDeviceScaleFactor =
    AutomationApiTestWithDeviceScaleFactor;

INSTANTIATE_TEST_SUITE_P(PlatformApp,
                         AutomationApiPlatformAppTestWithDeviceScaleFactor,
                         ::testing::Values(ContextType::kNone));

IN_PROC_BROWSER_TEST_P(AutomationApiPlatformAppTestWithDeviceScaleFactor,
                       LocationScaled) {
  StartEmbeddedTestServer();
  ASSERT_TRUE(RunExtensionTest("automation/tests/location_scaled",
                               {.launch_as_platform_app = true}))
      << message_;
}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithDeviceScaleFactor, HitTest) {
  StartEmbeddedTestServer();
  ASSERT_TRUE(
      CreateExtensionAndRunTest("desktop/hit_test.js", kPermissionsWindows))
      << message_;
}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, Position) {
  StartEmbeddedTestServer();
  ASSERT_TRUE(
      CreateExtensionAndRunTest("desktop/position.js", kPermissionsWindows))
      << message_;
}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, AccessibilityFocus) {
  StartEmbeddedTestServer();
  ASSERT_TRUE(CreateExtensionAndRunTest("tabs/accessibility_focus.js"))
      << message_;
}

// TODO(http://crbug.com/1162238): flaky on ChromeOS.
IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType,
                       DISABLED_TextareaAppendPerf) {
  StartEmbeddedTestServer();

  {
    base::RunLoop wait_for_tracing;
    content::TracingController::GetInstance()->StartTracing(
        base::trace_event::TraceConfig(
            R"({"included_categories": ["accessibility"])"),
        wait_for_tracing.QuitClosure());
    wait_for_tracing.Run();
  }

  ASSERT_TRUE(CreateExtensionAndRunTest("tabs/textarea_append_perf.js"))
      << message_;

  base::test::TestFuture<std::unique_ptr<std::string>> stop_tracing_future;
  content::TracingController::GetInstance()->StopTracing(
      content::TracingController::CreateStringEndpoint(
          stop_tracing_future.GetCallback()));

  std::optional<base::Value> trace_data =
      base::JSONReader::Read(*stop_tracing_future.Take());
  ASSERT_TRUE(trace_data && trace_data->is_dict());

  const base::Value::List* trace_events =
      trace_data->GetDict().FindList("traceEvents");
  ASSERT_TRUE(trace_events);

  int renderer_total_dur = 0;
  int automation_total_dur = 0;
  for (const base::Value& event : *trace_events) {
    const std::string* cat = event.GetDict().FindString("cat");
    if (!cat || *cat != "accessibility")
      continue;

    const std::string* name = event.GetDict().FindString("name");
    if (!name)
      continue;

    std::optional<int> dur = event.GetDict().FindInt("dur");
    if (!dur)
      continue;

    if (*name == "AutomationAXTreeWrapper::OnAccessibilityEvents")
      automation_total_dur += *dur;
    else if (*name == "RenderAccessibilityImpl::SendPendingAccessibilityEvents")
      renderer_total_dur += *dur;
  }

  ASSERT_GT(automation_total_dur, 0);
  ASSERT_GT(renderer_total_dur, 0);
  LOG(INFO) << "Total duration in automation: " << automation_total_dur;
  LOG(INFO) << "Total duration in renderer: " << renderer_total_dur;

  // Assert that the time spent in automation isn't more than 2x
  // the time spent in the renderer code.
  ASSERT_LT(automation_total_dur, renderer_total_dur * 2);
}

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType, IframeNav) {
  StartEmbeddedTestServer();
  ASSERT_TRUE(
      CreateExtensionAndRunTest("desktop/iframenav.js", kPermissionsWindows))
      << message_;
}

// TODO(crbug.com/1325383): test is flaky on Chromium OS MSAN builder.
#if BUILDFLAG(IS_CHROMEOS) && defined(MEMORY_SANITIZER)
#define MAYBE_AddRemoveEventListeners
#else
#define MAYBE_AddRemoveEventListeners
#endif
IN_PROC_BROWSER_TEST_F(AutomationApiTest, MAYBE_AddRemoveEventListeners) {
  StartEmbeddedTestServer();
  ASSERT_TRUE(
      RunExtensionTest("automation/tests/desktop",
                       {.extension_url = "add_remove_event_listeners.html"}))
      << message_;
}

class AutomationApiTestWithMockedSourceRenderer
    : public AutomationApiTestWithContextType,
      public ui::AXActionHandlerObserver {
 protected:
  // This method is used to intercept AXActions dispatched from extensions.
  // Because `DispatchActionResult`, from the automation API, is only used in
  // specific source renderers (e.g. arc++), we mock the behavior here so we can
  // test that the behavior in the automation api works correctly.
  void InterceptAXActions() {
    ui::AXActionHandlerRegistry* registry =
        ui::AXActionHandlerRegistry ::GetInstance();
    ASSERT_TRUE(registry);
    registry->AddObserver(this);
  }

 private:
  // ui::AXActionHandlerObserver :
  void PerformAction(const ui::AXActionData& action_data) override {
    extensions::AutomationEventRouter* router =
        extensions::AutomationEventRouter::GetInstance();
    ASSERT_TRUE(router);
    EXPECT_EQ(action_data.action, ax::mojom::Action::kScrollBackward);
    router->DispatchActionResult(action_data, /*result=*/true);
  }
};

INSTANTIATE_TEST_SUITE_P(PersistentBackground,
                         AutomationApiTestWithMockedSourceRenderer,
                         ::testing::Values(ContextType::kPersistentBackground));
INSTANTIATE_TEST_SUITE_P(ServiceWorker,
                         AutomationApiTestWithMockedSourceRenderer,
                         ::testing::Values(ContextType::kServiceWorker));

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithMockedSourceRenderer,
                       ActionResult) {
  StartEmbeddedTestServer();

  // Intercept AXActions for this test in order to test the behavior of
  // DispatchActionResult. Here, we mock the action logic to always return true
  // to return to the extension test that the action was handled and that the
  // result is true. This will make sure that the passing of messages between
  // processes is correct.
  InterceptAXActions();
  ASSERT_TRUE(CreateExtensionAndRunTest("desktop/action_result.js",
                                        kPermissionsWindows))
      << message_;
}
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)

#if BUILDFLAG(IS_CHROMEOS)
// TODO(crbug.com/40766689) Flaky on lacros
#if BUILDFLAG(IS_CHROMEOS_LACROS)
#define MAYBE_HitTestMultipleWindows
#else
#define MAYBE_HitTestMultipleWindows
#endif

IN_PROC_BROWSER_TEST_P(AutomationApiTestWithContextType,
                       MAYBE_HitTestMultipleWindows) {
  StartEmbeddedTestServer();
  ASSERT_TRUE(CreateExtensionAndRunTest("desktop/hit_test_multiple_windows.js",
                                        kPermissionsWindows))
      << message_;
}
#endif  // BUILDFLAG(IS_CHROMEOS)

}  // namespace extensions