chromium/chrome/browser/apps/guest_view/web_view_interactive_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.

#include <stddef.h>

#include <limits>
#include <memory>

#include "base/functional/bind.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/run_until.h"
#include "base/test/test_timeouts.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/apps/platform_apps/app_browsertest_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/renderer_context_menu/render_view_context_menu_browsertest_util.h"
#include "chrome/browser/renderer_context_menu/render_view_context_menu_test_util.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/test/base/interactive_test_utils.h"
#include "chrome/test/base/test_launcher_utils.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/guest_view/browser/guest_view_base.h"
#include "components/guest_view/browser/guest_view_manager.h"
#include "components/guest_view/browser/guest_view_manager_delegate.h"
#include "components/guest_view/browser/guest_view_manager_factory.h"
#include "components/guest_view/browser/test_guest_view_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_iterator.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/hit_test_region_observer.h"
#include "content/public/test/test_frame_navigation_observer.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/text_input_test_utils.h"
#include "extensions/browser/api/extensions_api_client.h"
#include "extensions/browser/app_window/app_window.h"
#include "extensions/browser/app_window/app_window_registry.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 "third_party/blink/public/common/switches.h"
#include "ui/base/buildflags.h"
#include "ui/base/ime/composition_text.h"
#include "ui/base/ime/ime_text_span.h"
#include "ui/base/ime/text_input_client.h"
#include "ui/base/ozone_buildflags.h"
#include "ui/base/test/ui_controls.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/range/range.h"
#include "ui/touch_selection/touch_selection_menu_runner.h"

#if BUILDFLAG(IS_MAC)
#include "ui/base/test/scoped_fake_nswindow_fullscreen.h"
#endif

AppWindow;
ExtensionsAPIClient;
GuestViewBase;
GuestViewManager;
TestGuestViewManager;
TestGuestViewManagerFactory;

#if !BUILDFLAG(IS_OZONE_WAYLAND)
// Some test helpers, like ui_test_utils::SendMouseMoveSync, don't work properly
// on some platforms. Tests that require these helpers need to be skipped for
// these cases.
#define SUPPORTS_SYNC_MOUSE_UTILS
#endif

#if BUILDFLAG(IS_MAC)
// This class observes the RenderWidgetHostViewCocoa corresponding to the outer
// most WebContents provided for newly added subviews. The added subview
// corresponds to a NSPopUpButtonCell which will be removed shortly after being
// shown.
class NewSubViewAddedObserver : content::RenderWidgetHostViewCocoaObserver {
 public:
  explicit NewSubViewAddedObserver(content::WebContents* web_contents)
      : content::RenderWidgetHostViewCocoaObserver(web_contents) {}

  NewSubViewAddedObserver(const NewSubViewAddedObserver&) = delete;
  NewSubViewAddedObserver& operator=(const NewSubViewAddedObserver&) = delete;
  ~NewSubViewAddedObserver() override {}

  void WaitForNextSubView() {
    if (did_receive_rect_)
      return;

    run_loop_ = std::make_unique<base::RunLoop>();
    run_loop_->Run();
  }

  const gfx::Rect& view_bounds_in_screen() const { return bounds_; }

 private:
  void DidAddSubviewWillBeDismissed(
      const gfx::Rect& bounds_in_root_view) override {
    did_receive_rect_ = true;
    bounds_ = bounds_in_root_view;
    if (run_loop_)
      run_loop_->Quit();
  }

  bool did_receive_rect_ = false;
  gfx::Rect bounds_;
  std::unique_ptr<base::RunLoop> run_loop_;
};
#endif  // BUILDFLAG(IS_MAC)

class WebViewInteractiveTest : public extensions::PlatformAppBrowserTest {};

class WebViewImeInteractiveTest : public WebViewInteractiveTest {};

class WebViewNewWindowInteractiveTest : public WebViewInteractiveTest {};
class WebViewFocusInteractiveTest : public WebViewInteractiveTest {};
class WebViewPointerLockInteractiveTest : public WebViewInteractiveTest {};

// The following class of tests do not work for OOPIF <webview>.
// TODO(ekaramad): Make this tests work with OOPIF and replace the test classes
// with WebViewInteractiveTest (see crbug.com/582562).
class DISABLED_WebViewPopupInteractiveTest : public WebViewInteractiveTest {};

// Timeouts flakily: crbug.com/1003345
#if defined(SUPPORTS_SYNC_MOUSE_UTILS) && !BUILDFLAG(IS_CHROMEOS) && \
    !BUILDFLAG(IS_MAC) && !BUILDFLAG(IS_WIN) && defined(NDEBUG)
#define MAYBE_PointerLock
#else
#define MAYBE_PointerLock
#endif
IN_PROC_BROWSER_TEST_F(WebViewPointerLockInteractiveTest, MAYBE_PointerLock) {}

// flaky http://crbug.com/412086
#if defined(SUPPORTS_SYNC_MOUSE_UTILS) && !BUILDFLAG(IS_CHROMEOS) && \
    !BUILDFLAG(IS_MAC) && defined(NDEBUG)
#define MAYBE_PointerLockFocus
#else
#define MAYBE_PointerLockFocus
#endif
IN_PROC_BROWSER_TEST_F(WebViewPointerLockInteractiveTest,
                       MAYBE_PointerLockFocus) {}

// Tests that if a <webview> is focused before navigation then the guest starts
// off focused.
// TODO(crbug.com/346863842): Flaky on linux-rel.
#if BUILDFLAG(IS_LINUX) && defined(NDEBUG)
#define MAYBE_Focus_FocusBeforeNavigation
#else
#define MAYBE_Focus_FocusBeforeNavigation
#endif
IN_PROC_BROWSER_TEST_F(WebViewFocusInteractiveTest,
                       MAYBE_Focus_FocusBeforeNavigation) {}

// Tests that setting focus on the <webview> sets focus on the guest.
IN_PROC_BROWSER_TEST_F(WebViewFocusInteractiveTest, Focus_FocusEvent) {}

// TODO(crbug.com/334045674): Flaky timeouts on Mac.
#if BUILDFLAG(IS_MAC)
#define MAYBE_Focus_FocusTakeFocus
#else
#define MAYBE_Focus_FocusTakeFocus
#endif
IN_PROC_BROWSER_TEST_F(WebViewFocusInteractiveTest,
                       MAYBE_Focus_FocusTakeFocus) {}

// Flaky on Mac and Linux - https://crbug.com/707648
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
#define MAYBE_Focus_FocusTracksEmbedder
#else
#define MAYBE_Focus_FocusTracksEmbedder
#endif

IN_PROC_BROWSER_TEST_F(WebViewFocusInteractiveTest,
                       MAYBE_Focus_FocusTracksEmbedder) {}

IN_PROC_BROWSER_TEST_F(WebViewFocusInteractiveTest, Focus_AdvanceFocus) {}

// Tests that blurring <webview> also blurs the guest.
IN_PROC_BROWSER_TEST_F(WebViewFocusInteractiveTest, Focus_BlurEvent) {}

// Tests that a <webview> can't steal focus from the embedder.
// TODO(crbug.com/349299938): Flaky on mac14-arm64-rel
#if BUILDFLAG(IS_MAC) && defined(NDEBUG)
#define MAYBE_FrameInGuestWontStealFocus
#else
#define MAYBE_FrameInGuestWontStealFocus
#endif
IN_PROC_BROWSER_TEST_F(WebViewFocusInteractiveTest,
                       MAYBE_FrameInGuestWontStealFocus) {}

// Tests that guests receive edit commands and respond appropriately.
IN_PROC_BROWSER_TEST_F(WebViewInteractiveTest, EditCommands) {}

// Tests that guests receive edit commands and respond appropriately.
IN_PROC_BROWSER_TEST_F(WebViewInteractiveTest, EditCommandsNoMenu) {}

// There is a problem of missing keyup events with the command key after
// the NSEvent is sent to NSApplication in ui/base/test/ui_controls_mac.mm .
// This test is disabled on only the Mac until the problem is resolved.
// See http://crbug.com/425859 for more information.
#if BUILDFLAG(IS_MAC)
#define MAYBE_NewWindow_OpenInNewTab
#else
#define MAYBE_NewWindow_OpenInNewTab
#endif
// Tests that Ctrl+Click/Cmd+Click on a link fires up the newwindow API.
IN_PROC_BROWSER_TEST_F(WebViewInteractiveTest, MAYBE_NewWindow_OpenInNewTab) {}

IN_PROC_BROWSER_TEST_F(DISABLED_WebViewPopupInteractiveTest,
                       PopupPositioningBasic) {}

// Flaky on ChromeOS and Linux: http://crbug.com/526886
// TODO(crbug.com/40560638): Flaky on Mac.
// TODO(crbug.com/41369000): Flaky on Windows.
// Tests that moving browser plugin (without resize/UpdateRects) correctly
// repositions popup.
IN_PROC_BROWSER_TEST_F(DISABLED_WebViewPopupInteractiveTest,
                       PopupPositioningMoved) {}

IN_PROC_BROWSER_TEST_F(WebViewInteractiveTest, Navigation) {}

IN_PROC_BROWSER_TEST_F(WebViewInteractiveTest, Navigation_BackForwardKeys) {}

IN_PROC_BROWSER_TEST_F(WebViewPointerLockInteractiveTest,
                       PointerLock_PointerLockLostWithFocus) {}

IN_PROC_BROWSER_TEST_F(WebViewInteractiveTest,
                       FullscreenAllow_EmbedderHasPermission) {}

IN_PROC_BROWSER_TEST_F(WebViewInteractiveTest,
                       FullscreenDeny_EmbedderHasPermission) {}

IN_PROC_BROWSER_TEST_F(WebViewInteractiveTest,
                       FullscreenAllow_EmbedderHasNoPermission) {}

IN_PROC_BROWSER_TEST_F(WebViewInteractiveTest,
                       FullscreenDeny_EmbedderHasNoPermission) {}

// This test exercies the following scenario:
// 1. An <input> in guest has focus.
// 2. User takes focus to embedder by clicking e.g. an <input> in embedder.
// 3. User brings back the focus directly to the <input> in #1.
//
// Now we need to make sure TextInputTypeChanged fires properly for the guest's
// view upon step #3. We simply read the input type's state after #3 to
// make sure it's not TEXT_INPUT_TYPE_NONE.
IN_PROC_BROWSER_TEST_F(WebViewFocusInteractiveTest, Focus_FocusRestored) {}

// ui::TextInputClient is NULL for mac and android.
#if !BUILDFLAG(IS_MAC) && !BUILDFLAG(IS_ANDROID)
#if defined(ADDRESS_SANITIZER) || BUILDFLAG(IS_WIN)
#define MAYBE_Focus_InputMethod
#else
#define MAYBE_Focus_InputMethod
#endif
IN_PROC_BROWSER_TEST_F(WebViewInteractiveTest, MAYBE_Focus_InputMethod) {}
#endif

#if BUILDFLAG(IS_LINUX) || \
    BUILDFLAG(IS_CHROMEOS)  // TODO(crbug.com/41364503): Flaky.
#define MAYBE_LongPressSelection
#else
#define MAYBE_LongPressSelection
#endif
#if !BUILDFLAG(IS_MAC)
IN_PROC_BROWSER_TEST_F(WebViewInteractiveTest, MAYBE_LongPressSelection) {}
#endif

#if BUILDFLAG(IS_MAC)
IN_PROC_BROWSER_TEST_F(WebViewInteractiveTest, TextSelection) {
  SetupTest("web_view/text_selection",
            "/extensions/platform_apps/web_view/text_selection/guest.html");
  ASSERT_TRUE(GetGuestView());
  ASSERT_TRUE(ui_test_utils::ShowAndFocusNativeWindow(
      GetPlatformAppWindow()));

  // Wait until guest sees a context menu.
  ExtensionTestMessageListener ctx_listener("MSG_CONTEXTMENU");
  ContextMenuWaiter menu_observer;
  SimulateRWHMouseClick(GetGuestRenderFrameHost()->GetRenderWidgetHost(),
                        blink::WebMouseEvent::Button::kRight, 20, 20);
  menu_observer.WaitForMenuOpenAndClose();
  ASSERT_TRUE(ctx_listener.WaitUntilSatisfied());

  // Now verify that the selection text propagates properly to RWHV.
  content::RenderWidgetHostView* guest_rwhv =
      GetGuestRenderFrameHost()->GetView();
  ASSERT_TRUE(guest_rwhv);
  std::string selected_text = base::UTF16ToUTF8(guest_rwhv->GetSelectedText());
  ASSERT_GE(selected_text.size(), 10u);
  ASSERT_EQ("AAAAAAAAAA", selected_text.substr(0, 10));
}

// Verifies that asking for a word lookup from a guest will lead to a returned
// mojo callback from the renderer containing the right selected word.
IN_PROC_BROWSER_TEST_F(WebViewInteractiveTest, WordLookup) {
  SetupTest("web_view/text_selection",
            "/extensions/platform_apps/web_view/text_selection/guest.html");
  ASSERT_TRUE(GetGuestView());
  ASSERT_TRUE(ui_test_utils::ShowAndFocusNativeWindow(GetPlatformAppWindow()));

  content::TextInputTestLocalFrame text_input_local_frame;
  text_input_local_frame.SetUp(GetGuestRenderFrameHost());

  // Lookup some string through context menu.
  ContextMenuNotificationObserver menu_observer(IDC_CONTENT_CONTEXT_LOOK_UP);
  // Simulating a mouse click at a position to highlight text in guest and
  // showing the context menu.
  SimulateRWHMouseClick(GetGuestRenderFrameHost()->GetRenderWidgetHost(),
                        blink::WebMouseEvent::Button::kRight, 20, 20);
  // Wait for the response form the guest renderer.
  text_input_local_frame.WaitForGetStringForRange();

  // Sanity check.
  ASSERT_EQ("AAAA", text_input_local_frame.GetStringFromRange().substr(0, 4));
}
#endif

// Flaky on Mac: http://crbug.com/811893
// Flaky on Linux/ChromeOS/Windows: http://crbug.com/845638
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || \
    BUILDFLAG(IS_WIN)
#define MAYBE_FocusAndVisibility
#else
#define MAYBE_FocusAndVisibility
#endif

IN_PROC_BROWSER_TEST_F(WebViewFocusInteractiveTest, MAYBE_FocusAndVisibility) {}

// Flaky timeouts on Linux. https://crbug.com/709202
// Flaky timeouts on Win. https://crbug.com/846695
// Flaky timeouts on Mac. https://crbug.com/1520415
IN_PROC_BROWSER_TEST_F(WebViewInteractiveTest, DISABLED_KeyboardFocusSimple) {}

// Ensures that input is routed to the webview after the containing window loses
// and regains focus. Additionally, the webview does not process keypresses sent
// while another window is focused.
// http://crbug.com/660044.
// Flaky on MacOSX, crbug.com/817067.
// Flaky on linux, crbug.com/706830.
// Flaky on Windows, crbug.com/847201.
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_MAC) || \
    BUILDFLAG(IS_WIN)
#define MAYBE_KeyboardFocusWindowCycle
#else
#define MAYBE_KeyboardFocusWindowCycle
#endif
IN_PROC_BROWSER_TEST_F(WebViewInteractiveTest, MAYBE_KeyboardFocusWindowCycle) {}

// Ensure that destroying a <webview> with a pending mouse lock request doesn't
// leave a stale mouse lock widget pointer in the embedder WebContents. See
// https://crbug.com/1346245.
// Flaky: crbug.com/1424552
IN_PROC_BROWSER_TEST_F(WebViewInteractiveTest,
                       DISABLED_DestroyGuestWithPendingPointerLock) {}

#if BUILDFLAG(IS_MAC)
// This test verifies that replacement range for IME works with <webview>s. To
// verify this, a <webview> with an <input> inside is loaded. Then the <input>
// is focused and  populated with some text. The test then sends an IPC to
// commit some text which will replace part of the previous text some new text.
IN_PROC_BROWSER_TEST_F(WebViewImeInteractiveTest,
                       CommitTextWithReplacementRange) {
  ASSERT_TRUE(StartEmbeddedTestServer());  // For serving guest pages.
  LoadAndLaunchPlatformApp("web_view/ime", "WebViewImeTest.Launched");
  ASSERT_TRUE(ui_test_utils::ShowAndFocusNativeWindow(GetPlatformAppWindow()));

  // Flush any pending events to make sure we start with a clean slate.
  content::RunAllPendingInMessageLoop();

  auto* guest_rfh = GetGuestViewManager()->GetLastGuestRenderFrameHostCreated();

  // Click the <input> element inside the <webview>. In its focus handle, the
  // <input> inside the <webview> initializes its value to "A B X D".
  ExtensionTestMessageListener focus_listener("WebViewImeTest.InputFocused");
  WaitForHitTestData(guest_rfh);

  // The guest page has a large input box and (50, 50) lies inside the box.
  content::SimulateMouseClickAt(
      GetGuestViewManager()->GetLastGuestViewCreated()->embedder_web_contents(),
      0, blink::WebMouseEvent::Button::kLeft,
      guest_rfh->GetView()->TransformPointToRootCoordSpace(gfx::Point(50, 50)));
  EXPECT_TRUE(focus_listener.WaitUntilSatisfied());

  // Verify the text inside the <input> is "A B X D".
  EXPECT_EQ("A B X D", content::EvalJs(guest_rfh,
                                       "document.querySelector('"
                                       "input').value"));

  // Now commit "C" to to replace the range (4, 5).
  // For OOPIF guests, the target for IME is the RWH for the guest's main frame.
  // For BrowserPlugin-based guests, input always goes to the embedder.
  ExtensionTestMessageListener input_listener("WebViewImetest.InputReceived");
  content::SendImeCommitTextToWidget(guest_rfh->GetRenderWidgetHost(), u"C",
                                     std::vector<ui::ImeTextSpan>(),
                                     gfx::Range(4, 5), 0);
  EXPECT_TRUE(input_listener.WaitUntilSatisfied());

  // Get the input value from the guest.
  EXPECT_EQ("A B C D", content::EvalJs(guest_rfh,
                                       "document.querySelector('"
                                       "input').value"));
}
#endif  // BUILDFLAG(IS_MAC)

// This test verifies that focusing an input inside a <webview> will put the
// guest process's render widget into a monitoring mode for composition range
// changes.
IN_PROC_BROWSER_TEST_F(WebViewImeInteractiveTest, CompositionRangeUpdates) {}

#if BUILDFLAG(IS_MAC)
// This test verifies that drop-down lists appear correctly inside OOPIF-based
// webviews which have offset inside embedder. This is a test for all guest
// views as the logic for showing such popups is inside content/ layer. For more
// context see https://crbug.com/772840.
IN_PROC_BROWSER_TEST_F(WebViewFocusInteractiveTest,
                       DropDownPopupInCorrectPosition) {
  TestHelper("testSelectPopupPositionInMac", "web_view/shim", NO_TEST_SERVER);
  ASSERT_TRUE(GetGuestRenderFrameHost());

  // This is set in javascript.
  const float distance_from_root_view_origin = 250.0;
  // Verify that the view is offset inside root view as expected.
  content::RenderWidgetHostView* guest_rwhv =
      GetGuestRenderFrameHost()->GetView();
  EXPECT_TRUE(base::test::RunUntil([&]() {
    return guest_rwhv->TransformPointToRootCoordSpace(gfx::Point())
               .OffsetFromOrigin()
               .Length() >= distance_from_root_view_origin;
  }));

  // Now trigger the popup and wait until it is displayed. The popup will get
  // dismissed after being shown.
  NewSubViewAddedObserver popup_observer(embedder_web_contents_);
  // Now send a mouse click and wait until the <select> tag is focused.
  SimulateRWHMouseClick(guest_rwhv->GetRenderWidgetHost(),
                        blink::WebMouseEvent::Button::kLeft, 5, 5);
  popup_observer.WaitForNextSubView();

  // Verify the popup bounds intersect with those of the guest. Since the popup
  // is relatively small (the width is determined by the <select> element's
  // width and the hight is a factor of font-size and number of items), the
  // intersection alone is a good indication the popup is shown properly inside
  // the screen.
  gfx::Rect guest_bounds_in_embedder(
      guest_rwhv->TransformPointToRootCoordSpace(gfx::Point()),
      guest_rwhv->GetViewBounds().size());
  EXPECT_TRUE(guest_bounds_in_embedder.Intersects(
      popup_observer.view_bounds_in_screen()))
      << "Guest bounds:" << guest_bounds_in_embedder.ToString()
      << " do not intersect with popup bounds:"
      << popup_observer.view_bounds_in_screen().ToString();
}
#endif

// Check that when a focused <webview> navigates cross-process, the focus
// is preserved in the new page. See https://crbug.com/1358210.
IN_PROC_BROWSER_TEST_F(WebViewInteractiveTest,
                       FocusPreservedAfterCrossProcessNavigation) {}