chromium/chrome/browser/ui/views/omnibox/omnibox_view_views_browsertest.cc

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

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "chrome/browser/ui/views/omnibox/omnibox_view_views.h"

#include <stddef.h>

#include "base/feature_list.h"
#include "base/memory/raw_ptr.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/external_protocol/external_protocol_handler.h"
#include "chrome/browser/interstitials/security_interstitial_page_test_utils.h"
#include "chrome/browser/search_engines/template_url_service_factory.h"
#include "chrome/browser/ssl/typed_navigation_upgrade_throttle.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/location_bar/location_bar.h"
#include "chrome/browser/ui/view_ids.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/location_bar/location_bar_view.h"
#include "chrome/browser/ui/views/omnibox/omnibox_popup_view_views.h"
#include "chrome/browser/ui/views/toolbar/toolbar_view.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/interactive_test_utils.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/omnibox/browser/autocomplete_match.h"
#include "components/omnibox/browser/omnibox_controller.h"
#include "components/omnibox/browser/omnibox_edit_model.h"
#include "components/omnibox/browser/omnibox_triggered_feature_service.h"
#include "components/omnibox/browser/test_scheme_classifier.h"
#include "components/security_interstitials/core/omnibox_https_upgrade_metrics.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/no_renderer_crashes_assertion.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/url_loader_interceptor.h"
#include "third_party/blink/public/common/chrome_debug_urls.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/clipboard/clipboard.h"
#include "ui/base/clipboard/scoped_clipboard_writer.h"
#include "ui/base/dragdrop/drag_drop_types.h"
#include "ui/base/ime/init/input_method_factory.h"
#include "ui/base/ime/input_method.h"
#include "ui/base/ime/mock_input_method.h"
#include "ui/base/ime/text_edit_commands.h"
#include "ui/base/ime/text_input_client.h"
#include "ui/base/test/ui_controls.h"
#include "ui/base/ui_base_switches.h"
#include "ui/display/display_switches.h"
#include "ui/events/event_processor.h"
#include "ui/events/event_utils.h"
#include "ui/events/test/event_generator.h"
#include "ui/views/accessibility/atomic_view_ax_tree_manager.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/textfield/textfield_test_api.h"
#include "ui/views/views_features.h"

#if defined(USE_AURA)
#include "ui/aura/window.h"
#include "ui/aura/window_tree_host.h"
#endif

#if BUILDFLAG(IS_WIN)
#include "base/win/scoped_bstr.h"
#include "base/win/scoped_safearray.h"
#include "chrome/browser/ui/views/accessibility/uia_accessibility_event_waiter.h"
#include "ui/display/win/screen_win.h"
#include "ui/views/win/hwnd_util.h"
#endif

namespace {

void SetClipboardText(ui::ClipboardBuffer buffer, const std::u16string& text) {}

}  // namespace

class OmniboxViewViewsTest : public InProcessBrowserTest {};

IN_PROC_BROWSER_TEST_F(OmniboxViewViewsTest, PasteAndGoDoesNotLeavePopupOpen) {}

IN_PROC_BROWSER_TEST_F(OmniboxViewViewsTest, DoNotNavigateOnDrop) {}

IN_PROC_BROWSER_TEST_F(OmniboxViewViewsTest, AyncDropCallback) {}

// Flaky: https://crbug.com/915591.
IN_PROC_BROWSER_TEST_F(OmniboxViewViewsTest, DISABLED_SelectAllOnClick) {}

// TODO(crbug.com/339098004): Flaky across multiple builders.
IN_PROC_BROWSER_TEST_F(OmniboxViewViewsTest, DISABLED_SelectionClipboard) {}

// No touch on desktop Mac. Tracked in http://crbug.com/445520.
#if !BUILDFLAG(IS_MAC) || defined(USE_AURA)

IN_PROC_BROWSER_TEST_F(OmniboxViewViewsTest, SelectAllOnTap) {}

// Tests if executing a command hides touch editing handles.
IN_PROC_BROWSER_TEST_F(OmniboxViewViewsTest,
                       DeactivateTouchEditingOnExecuteCommand) {}

#endif  // !BUILDFLAG(IS_MAC) || defined(USE_AURA)

IN_PROC_BROWSER_TEST_F(OmniboxViewViewsTest, SelectAllOnTabToFocus) {}

IN_PROC_BROWSER_TEST_F(OmniboxViewViewsTest, CloseOmniboxPopupOnTextDrag) {}

IN_PROC_BROWSER_TEST_F(OmniboxViewViewsTest, MaintainCursorAfterFocusCycle) {}

IN_PROC_BROWSER_TEST_F(OmniboxViewViewsTest, BackgroundIsOpaque) {}

IN_PROC_BROWSER_TEST_F(OmniboxViewViewsTest, FocusedTextInputClient) {}

IN_PROC_BROWSER_TEST_F(OmniboxViewViewsTest, TextElideStatus) {}

IN_PROC_BROWSER_TEST_F(OmniboxViewViewsTest, FragmentUnescapedForDisplay) {}

// Ensure that when the user navigates between suggestions, that the accessible
// value of the Omnibox includes helpful information for human comprehension,
// such as the document title. When the user begins to arrow left/right
// within the label or edit it, the value is presented as the pure editable URL.
IN_PROC_BROWSER_TEST_F(OmniboxViewViewsTest, FriendlyAccessibleLabel) {}

// Ensure that the Omnibox popup exposes appropriate accessibility semantics,
// given a current state of open or closed.
IN_PROC_BROWSER_TEST_F(OmniboxViewViewsTest, AccessiblePopup) {}

// Flaky: https://crbug.com/1143630.
// Omnibox returns to clean state after chrome://kill and reload.
// https://crbug.com/993701 left the URL and icon as chrome://kill after reload.
IN_PROC_BROWSER_TEST_F(OmniboxViewViewsTest, DISABLED_ReloadAfterKill) {}

// Omnibox un-elides and elides URL appropriately according to the Always Show
// Full URLs setting.
IN_PROC_BROWSER_TEST_F(OmniboxViewViewsTest, AlwaysShowFullURLs) {}

// The following set of tests require UIA accessibility support, which only
// exists on Windows.
#if BUILDFLAG(IS_WIN)
using Microsoft::WRL::ComPtr;

class OmniboxViewViewsUIATest : public OmniboxViewViewsTest {
 public:
  OmniboxViewViewsUIATest() {}

  void ExpectUIADoubleSafearrayEQ(
      SAFEARRAY* safearray,
      const std::vector<double>& expected_property_values) {
    EXPECT_EQ(sizeof(V_R8(LPVARIANT(NULL))), ::SafeArrayGetElemsize(safearray));
    ASSERT_EQ(1u, SafeArrayGetDim(safearray));
    LONG array_lower_bound;
    ASSERT_HRESULT_SUCCEEDED(
        SafeArrayGetLBound(safearray, 1, &array_lower_bound));
    LONG array_upper_bound;
    ASSERT_HRESULT_SUCCEEDED(
        SafeArrayGetUBound(safearray, 1, &array_upper_bound));
    double* array_data;
    ASSERT_HRESULT_SUCCEEDED(::SafeArrayAccessData(
        safearray, reinterpret_cast<void**>(&array_data)));
    size_t count = array_upper_bound - array_lower_bound + 1;
    ASSERT_EQ(expected_property_values.size(), count);
    for (size_t i = 0; i < count; ++i) {
      EXPECT_EQ(array_data[i], expected_property_values[i]);
    }
    ASSERT_HRESULT_SUCCEEDED(::SafeArrayUnaccessData(safearray));
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_{::features::kUiaProvider};
};

// Omnibox fires the right events when the popup opens/closes with UIA turned
// on.
IN_PROC_BROWSER_TEST_F(OmniboxViewViewsUIATest, AccessibleOmnibox) {
  OmniboxView* omnibox_view = nullptr;
  ASSERT_NO_FATAL_FAILURE(GetOmniboxViewForBrowser(browser(), &omnibox_view));
  chrome::FocusLocationBar(browser());

  std::u16string match_url = u"https://example.com";
  AutocompleteMatch match(nullptr, 500, false,
                          AutocompleteMatchType::HISTORY_TITLE);
  match.contents = match_url;
  match.contents_class.push_back(
      ACMatchClassification(0, ACMatchClassification::URL));
  match.destination_url = GURL(match_url);
  match.description = u"Example";
  match.allowed_to_be_default_match = true;

  EXPECT_FALSE(omnibox_view->model()->PopupIsOpen());

  HWND window_handle =
      browser()->window()->GetNativeWindow()->GetHost()->GetAcceleratedWidget();
  UiaAccessibilityWaiterInfo info = {window_handle, L"textbox",
                                     L"Address and search bar",
                                     ax::mojom::Event::kControlsChanged};
  UiaAccessibilityEventWaiter open_waiter(info);

  // Populate suggestions for the omnibox popup.
  AutocompleteController* autocomplete_controller =
      omnibox_view->controller()->autocomplete_controller();
  AutocompleteResult& results = autocomplete_controller->internal_result_;
  ACMatches matches;
  matches.push_back(match);
  AutocompleteInput input(u"e", metrics::OmniboxEventProto::OTHER,
                          TestSchemeClassifier());
  results.AppendMatches(matches);
  results.SortAndCull(
      input, TemplateURLServiceFactory::GetForProfile(browser()->profile()),
      triggered_feature_service());

  // The omnibox popup should open with suggestions displayed.
  autocomplete_controller->NotifyChanged();

  // Wait for ControllerFor property changed event.
  open_waiter.Wait();

  EXPECT_TRUE(omnibox_view->model()->PopupIsOpen());

  UiaAccessibilityEventWaiter close_waiter(info);
  // Close the popup. Another property change event is expected.
  ClickBrowserWindowCenter();
  close_waiter.Wait();
  EXPECT_FALSE(omnibox_view->model()->PopupIsOpen());
}

IN_PROC_BROWSER_TEST_F(OmniboxViewViewsUIATest, GetSelectionAndBounds) {
  OmniboxView* omnibox_view = nullptr;
  ASSERT_NO_FATAL_FAILURE(GetOmniboxViewForBrowser(browser(), &omnibox_view));
  OmniboxViewViews* omnibox_view_views =
      static_cast<OmniboxViewViews*>(omnibox_view);

  views::AtomicViewAXTreeManager* ax_tree_manager =
      omnibox_view_views->GetViewAccessibility()
          .GetAtomicViewAXTreeManagerForTesting();

  // Set the text and select a substring.
  gfx::Range selection_range(4, 8);

  omnibox_view_views->SetUserText(u"helloworld");
  omnibox_view_views->SetSelectedRange(selection_range);

  // Get the UIA providers we need to perform the tests.
  ui::AXPlatformNode* ax_platform_node =
      ui::AXPlatformNode::FromNativeViewAccessible(
          ax_tree_manager->RootDelegate()->GetNativeViewAccessible());

  ComPtr<IRawElementProviderSimple> root_node_raw;
  ax_platform_node->GetNativeViewAccessible()->QueryInterface(
      __uuidof(IRawElementProviderSimple), &root_node_raw);
  ComPtr<ITextProvider> document_provider;
  ASSERT_HRESULT_SUCCEEDED(
      root_node_raw->GetPatternProvider(UIA_TextPatternId, &document_provider));

  CComPtr<ITextRangeProvider> selected_text_range_provider;
  base::win::ScopedSafearray selection;
  LONG index = 0;

  // The actual testing starts:
  // 1. Call ITextProvider::GetSelection to get the selected text range.
  document_provider->GetSelection(selection.Receive());
  ASSERT_NE(nullptr, selection.Get());
  ASSERT_HRESULT_SUCCEEDED(SafeArrayGetElement(selection.Get(), &index,
                                               &selected_text_range_provider));

  // 2. Call ITextRangeProvider::GetText to get the selected text.
  base::win::ScopedBstr text_content;
  ASSERT_HRESULT_SUCCEEDED(
      selected_text_range_provider->GetText(-1, text_content.Receive()));
  EXPECT_STREQ(L"owor", text_content.Get());

  // 3. Call ITextRangeProvider::GetBoundingRectangles to get the bounding rect.
  base::win::ScopedSafearray safearray;
  ASSERT_HRESULT_SUCCEEDED(
      selected_text_range_provider->GetBoundingRectangles(safearray.Receive()));

  ComPtr<IRawElementProviderFragment> textfield_fragment;
  ax_platform_node->GetNativeViewAccessible()->QueryInterface(
      __uuidof(IRawElementProviderFragment), &textfield_fragment);
  UiaRect textfield_rect;

  ASSERT_HRESULT_SUCCEEDED(
      textfield_fragment->get_BoundingRectangle(&textfield_rect));

  // Create the expected bounds for the text range from the bounds of the text
  // field and the selected range offsets.
  gfx::Rect bounds_in_screen = omnibox_view_views->GetContentsBounds();
  omnibox_view_views->ConvertRectToScreen(omnibox_view_views,
                                          &bounds_in_screen);

  std::vector<int32_t> offsets =
      ax_tree_manager->GetRoot()->GetIntListAttribute(
          ax::mojom::IntListAttribute::kCharacterOffsets);
  int range_start_offset = offsets[selection_range.start()];
  int range_end_offset = offsets[selection_range.end()];

  int left_bound =
      display::win::ScreenWin::DIPToScreenRect(
          HWNDForView(omnibox_view_views),
          gfx::Rect(range_start_offset + bounds_in_screen.x(), 0, 0, 0))
          .x();

  // Adust `textfield_rect` to account for the border.
  textfield_rect.top += omnibox_view_views->GetInsets().top();
  textfield_rect.height -= omnibox_view_views->GetInsets().height();

  std::vector<double> expected_values = std::vector<double>{
      static_cast<double>(left_bound), textfield_rect.top,
      static_cast<double>(range_end_offset - range_start_offset),
      textfield_rect.height};

  ASSERT_NO_FATAL_FAILURE(
      ExpectUIADoubleSafearrayEQ(safearray.Get(), expected_values));
}

namespace {

class OmniboxMockInputMethod : public ui::MockInputMethod {
 public:
  OmniboxMockInputMethod() : ui::MockInputMethod(nullptr) {}
  // ui::MockInputMethod:
  bool IsInputLocaleCJK() const override { return input_locale_cjk_; }
  void SetInputLocaleCJK(bool is_cjk) { input_locale_cjk_ = is_cjk; }

 private:
  bool input_locale_cjk_ = false;
};

}  // namespace

class OmniboxViewViewsIMETest : public OmniboxViewViewsTest {
 public:
  OmniboxViewViewsIMETest() = default;

  // OmniboxViewViewsTest:
  void SetUp() override {
    input_method_ = new OmniboxMockInputMethod();
    // Transfers ownership.
    ui::SetUpInputMethodForTesting(input_method_);
    InProcessBrowserTest::SetUp();
  }

 protected:
  OmniboxMockInputMethod* GetInputMethod() const { return input_method_; }

 private:
  base::test::ScopedFeatureList scoped_feature_list_{::features::kUiaProvider};
  raw_ptr<OmniboxMockInputMethod, AcrossTasksDanglingUntriaged> input_method_ =
      nullptr;
};

IN_PROC_BROWSER_TEST_F(OmniboxViewViewsIMETest, TextInputTypeChangedTest) {
  chrome::FocusLocationBar(browser());
  OmniboxView* view = nullptr;
  ASSERT_NO_FATAL_FAILURE(GetOmniboxViewForBrowser(browser(), &view));
  OmniboxViewViews* omnibox_view_views = static_cast<OmniboxViewViews*>(view);
  ui::InputMethod* input_method =
      omnibox_view_views->GetWidget()->GetInputMethod();
  EXPECT_EQ(input_method, GetInputMethod());
  EXPECT_EQ(ui::TEXT_INPUT_TYPE_URL, omnibox_view_views->GetTextInputType());
  GetInputMethod()->SetInputLocaleCJK(/*is_cjk=*/true);
  omnibox_view_views->OnInputMethodChanged();
  EXPECT_EQ(ui::TEXT_INPUT_TYPE_SEARCH, omnibox_view_views->GetTextInputType());

  GetInputMethod()->SetInputLocaleCJK(/*is_cjk=*/false);
  omnibox_view_views->OnInputMethodChanged();
  EXPECT_EQ(ui::TEXT_INPUT_TYPE_URL, omnibox_view_views->GetTextInputType());
}

IN_PROC_BROWSER_TEST_F(OmniboxViewViewsIMETest, TextInputTypeInitRespectsIME) {
  OmniboxMockInputMethod* input_method = new OmniboxMockInputMethod();
  ui::SetUpInputMethodForTesting(input_method);
  input_method->SetInputLocaleCJK(/*is_cjk=*/true);
  Browser* browser_2 = CreateBrowser(browser()->profile());
  OmniboxView* view = nullptr;
  ASSERT_NO_FATAL_FAILURE(GetOmniboxViewForBrowser(browser_2, &view));
  OmniboxViewViews* omnibox_view_views = static_cast<OmniboxViewViews*>(view);
  EXPECT_EQ(omnibox_view_views->GetWidget()->GetInputMethod(), input_method);
  EXPECT_EQ(ui::TEXT_INPUT_TYPE_SEARCH, omnibox_view_views->GetTextInputType());
  input_method->SetInputLocaleCJK(/*is_cjk=*/false);
  omnibox_view_views->OnInputMethodChanged();
  EXPECT_EQ(ui::TEXT_INPUT_TYPE_URL, omnibox_view_views->GetTextInputType());
}
#endif  // BUILDFLAG(IS_WIN)

// ClickOnView(VIEW_ID_OMNIBOX) does not set focus to omnibox on Mac.
// Looks like the same problem as in the SelectAllOnClick().
// Tracked in: https://crbug.com/915591
// Test is also flaky on Linux: https://crbug.com/1157250
// Click goes to the popup widget, but doesn't sets focus to the popup view if
// it's a separate accelerated widget on Lacros: https://crbug.com/329271186
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS)
#define MAYBE_HandleExternalProtocolURLs
#else
#define MAYBE_HandleExternalProtocolURLs
#endif

// Checks that focusing on the omnibox allows the page to open external protocol
// URLs. Regression test for https://crbug.com/1143632
IN_PROC_BROWSER_TEST_F(OmniboxViewViewsTest, MAYBE_HandleExternalProtocolURLs) {}

// SendKeyPressSync times out on Mac, probably due to https://crbug.com/824418.
// TODO(crbug.com/332299695): Also fails on lacros.
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_CHROMEOS_LACROS)
#define MAYBE_DefaultTypedNavigationsToHttps_ZeroSuggest_NoUpgrade
#else
#define MAYBE_DefaultTypedNavigationsToHttps_ZeroSuggest_NoUpgrade
#endif

// Test that triggers a zero suggest autocomplete request by clicking on the
// omnibox. These should never attempt an HTTPS upgrade or involve the typed
// navigation upgrade throttle.
// This is a regression test for https://crbug.com/1251065
IN_PROC_BROWSER_TEST_F(
    OmniboxViewViewsTest,
    MAYBE_DefaultTypedNavigationsToHttps_ZeroSuggest_NoUpgrade) {}