910e62b5创建于 1月15日历史提交
// 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 "ui/views/controls/webview/webview.h"

#include <stdint.h>

#include <memory>
#include <utility>

#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.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_task_environment.h"
#include "content/public/test/mock_render_process_host.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/web_contents_tester.h"
#include "content/test/test_content_browser_client.h"
#include "ui/events/event.h"
#include "ui/events/event_utils.h"
#include "ui/gfx/native_ui_types.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/native/native_view_host.h"
#include "ui/views/test/views_test_utils.h"
#include "ui/views/test/widget_test.h"

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

namespace views {

namespace {

// Provides functionality to observe events on a WebContents like
// OnVisibilityChanged/WebContentsDestroyed.
class WebViewTestWebContentsObserver : public content::WebContentsObserver {
 public:
  explicit WebViewTestWebContentsObserver(content::WebContents* web_contents)
      : web_contents_(web_contents) {
    content::WebContentsObserver::Observe(web_contents);
  }

  WebViewTestWebContentsObserver(const WebViewTestWebContentsObserver&) =
      delete;
  WebViewTestWebContentsObserver& operator=(
      const WebViewTestWebContentsObserver&) = delete;

  ~WebViewTestWebContentsObserver() override {
    if (web_contents_) {
      content::WebContentsObserver::Observe(nullptr);
    }
  }

  void WebContentsDestroyed() override {
    DCHECK(web_contents_);
    content::WebContentsObserver::Observe(nullptr);
    web_contents_ = nullptr;
  }

  void OnVisibilityChanged(content::Visibility visibility) override {
    switch (visibility) {
      case content::Visibility::VISIBLE: {
#if defined(USE_AURA)
        valid_root_while_shown_ =
            web_contents()->GetNativeView()->GetRootWindow() != nullptr;
#endif
        was_shown_ = true;
        ++shown_count_;
        break;
      }
      case content::Visibility::HIDDEN: {
        was_shown_ = false;
        ++hidden_count_;
        break;
      }
      default: {
        ADD_FAILURE() << "Unexpected call to OnVisibilityChanged.";
        break;
      }
    }
  }

  bool was_shown() const { return was_shown_; }

  int shown_count() const { return shown_count_; }

  int hidden_count() const { return hidden_count_; }

  bool valid_root_while_shown() const { return valid_root_while_shown_; }

 private:
  raw_ptr<content::WebContents> web_contents_;
  bool was_shown_ = false;
  int32_t shown_count_ = 0;
  int32_t hidden_count_ = 0;
  // Set to true if the view containing the webcontents has a valid root window.
  bool valid_root_while_shown_ = true;
};

// Fakes the fullscreen browser state reported to WebContents and WebView.
class WebViewTestWebContentsDelegate : public content::WebContentsDelegate {
 public:
  WebViewTestWebContentsDelegate() = default;

  WebViewTestWebContentsDelegate(const WebViewTestWebContentsDelegate&) =
      delete;
  WebViewTestWebContentsDelegate& operator=(
      const WebViewTestWebContentsDelegate&) = delete;

  ~WebViewTestWebContentsDelegate() override = default;

  void set_is_fullscreened(bool fs) { is_fullscreened_ = fs; }

  // content::WebContentsDelegate overrides.
  bool IsFullscreenForTabOrPending(
      const content::WebContents* ignored) override {
    return is_fullscreened_;
  }

 private:
  bool is_fullscreened_ = false;
};

void SimulateRendererCrash(content::WebContents* contents, WebView* view) {
  auto* tester = content::WebContentsTester::For(contents);

  // Normally when a renderer crashes, the WebView will learn about it
  // automatically via WebContentsObserver. Since this is a test
  // WebContents, simulate that by calling SetIsCrashed and then
  // explicitly calling RenderFrameDeleted on the WebView to trigger it
  // to swap in the crashed overlay view.
  tester->SetIsCrashed(base::TERMINATION_STATUS_PROCESS_CRASHED, -1);
  EXPECT_TRUE(contents->IsCrashed());
  static_cast<content::WebContentsObserver*>(view)->RenderFrameDeleted(
      contents->GetPrimaryMainFrame());
}

}  // namespace

// Provides functionality to test a WebView.
class WebViewUnitTest : public views::test::WidgetTest {
 public:
  static constexpr int kWebViewID = 123;

  WebViewUnitTest()
      : views::test::WidgetTest(std::unique_ptr<base::test::TaskEnvironment>(
            std::make_unique<content::BrowserTaskEnvironment>())) {}

  WebViewUnitTest(const WebViewUnitTest&) = delete;
  WebViewUnitTest& operator=(const WebViewUnitTest&) = delete;

  ~WebViewUnitTest() override = default;

  std::unique_ptr<content::WebContents> CreateWebContentsForWebView(
      content::BrowserContext* browser_context) {
    return content::WebContentsTester::CreateTestWebContents(browser_context,
                                                             nullptr);
  }

  void SetUp() override {
    // Set the test content browser client to avoid pulling in needless
    // dependencies from content.
    SetBrowserClientForTesting(&test_browser_client_);

    rvh_enabler_ = std::make_unique<content::RenderViewHostTestEnabler>();

    views::WebView::WebContentsCreator creator = base::BindRepeating(
        &WebViewUnitTest::CreateWebContentsForWebView, base::Unretained(this));
    scoped_web_contents_creator_ =
        std::make_unique<views::WebView::ScopedWebContentsCreatorForTesting>(
            creator);
    browser_context_ = std::make_unique<content::TestBrowserContext>();
    WidgetTest::SetUp();

    base::CommandLine::ForCurrentProcess()->AppendSwitch(
        switches::kDisableBackgroundingOccludedWindowsForTesting);

    // Create a top level widget and add a child, and give it a WebView as a
    // child.
    top_level_widget_ = CreateTopLevelFramelessPlatformWidget();
    top_level_widget_->SetBounds(gfx::Rect(0, 10, 100, 100));
    View* const contents_view =
        top_level_widget_->SetContentsView(std::make_unique<View>());
    auto view = std::make_unique<WebView>(browser_context_.get());
    view->SetID(kWebViewID);
    view->SetBoundsRect(gfx::Rect(contents_view->size()));
    contents_view->AddChildView(std::move(view));
    top_level_widget_->Show();
    ASSERT_EQ(gfx::Rect(0, 0, 100, 100), web_view()->bounds());
  }

  void TearDown() override {
    scoped_web_contents_creator_.reset();
    top_level_widget_.ExtractAsDangling()
        ->Close();  // Deletes all children and itself.
    RunPendingMessages();

    browser_context_.reset(nullptr);
    // Flush the message loop to execute pending relase tasks as this would
    // upset ASAN and Valgrind.
    RunPendingMessages();
    WidgetTest::TearDown();
  }

 protected:
  Widget* top_level_widget() { return top_level_widget_; }
  WebView* web_view() {
    return static_cast<WebView*>(
        top_level_widget()->GetContentsView()->GetViewByID(kWebViewID));
  }
  NativeViewHost* holder() { return web_view()->holder_; }

  std::unique_ptr<content::WebContents> CreateWebContents() const {
    return content::WebContents::Create(
        content::WebContents::CreateParams(browser_context_.get()));
  }

  std::unique_ptr<content::WebContents> CreateTestWebContents() const {
    return content::WebContentsTester::CreateTestWebContents(
        browser_context_.get(), /*instance=*/nullptr);
  }

  void SetAXModeForView(WebView* view, ui::AXMode mode) {
    view->OnAXModeAdded(mode);
  }

 private:
  std::unique_ptr<content::RenderViewHostTestEnabler> rvh_enabler_;
  std::unique_ptr<content::TestBrowserContext> browser_context_;
  content::TestContentBrowserClient test_browser_client_;
  std::unique_ptr<views::WebView::ScopedWebContentsCreatorForTesting>
      scoped_web_contents_creator_;

  raw_ptr<Widget> top_level_widget_ = nullptr;
};

// Tests that attaching and detaching a WebContents to a WebView makes the
// WebContents visible and hidden respectively.
TEST_F(WebViewUnitTest, TestWebViewAttachDetachWebContents) {
  // Case 1: Create a new WebContents and set it in the webview via
  // SetWebContents. This should make the WebContents visible.
  const std::unique_ptr<content::WebContents> web_contents1(
      CreateWebContents());
  WebViewTestWebContentsObserver observer1(web_contents1.get());
  EXPECT_FALSE(observer1.was_shown());

  web_view()->SetWebContents(web_contents1.get());
  // Layout is normally async, ensure it runs now so visibility is updated.
  views::test::RunScheduledLayout(web_view());
  EXPECT_TRUE(observer1.was_shown());
#if defined(USE_AURA)
  EXPECT_TRUE(web_contents1->GetNativeView()->IsVisible());
#endif
  EXPECT_EQ(observer1.shown_count(), 1);
  EXPECT_EQ(observer1.hidden_count(), 0);
  EXPECT_TRUE(observer1.valid_root_while_shown());

  // Case 2: Create another WebContents and replace the current WebContents
  // via SetWebContents(). This should hide the current WebContents and show
  // the new one.
  const std::unique_ptr<content::WebContents> web_contents2(
      CreateWebContents());
  WebViewTestWebContentsObserver observer2(web_contents2.get());
  EXPECT_FALSE(observer2.was_shown());

  // Setting the new WebContents should hide the existing one.
  web_view()->SetWebContents(web_contents2.get());
  // Layout is normally async, ensure it runs now so visibility is updated.
  views::test::RunScheduledLayout(web_view());
  EXPECT_FALSE(observer1.was_shown());
  EXPECT_TRUE(observer2.was_shown());
  EXPECT_TRUE(observer2.valid_root_while_shown());

  // WebContents1 should not get stray show calls when WebContents2 is set.
  EXPECT_EQ(observer1.shown_count(), 1);
  EXPECT_EQ(observer1.hidden_count(), 1);
  EXPECT_EQ(observer2.shown_count(), 1);
  EXPECT_EQ(observer2.hidden_count(), 0);

  // Case 3: Test that attaching to a hidden webview does not show the web
  // contents.
  web_view()->SetVisible(false);
  EXPECT_EQ(1, observer2.hidden_count());  // Now hidden.

  EXPECT_EQ(1, observer1.shown_count());
  web_view()->SetWebContents(web_contents1.get());
  // Layout is normally async, ensure it runs now so visibility is updated.
  views::test::RunScheduledLayout(web_view());
  EXPECT_EQ(1, observer1.shown_count());

  // Nothing else should change.
  EXPECT_EQ(1, observer1.hidden_count());
  EXPECT_EQ(1, observer2.shown_count());
  EXPECT_EQ(1, observer2.hidden_count());

#if defined(USE_AURA)
  // Case 4: Test that making the webview visible when a window has an invisible
  // parent does not make the web contents visible.
  top_level_widget()->Hide();
  web_view()->SetVisible(true);
  EXPECT_EQ(1, observer1.shown_count());
  top_level_widget()->Show();
  EXPECT_EQ(2, observer1.shown_count());
  top_level_widget()->Hide();
  EXPECT_EQ(2, observer1.hidden_count());
#else
  // On Mac, changes to window visibility do not trigger calls to WebContents::
  // WasShown() or WasHidden(), since the OS does not provide good signals for
  // window visibility. However, we can still test that moving a visible WebView
  // whose WebContents is not currently showing to a new, visible window will
  // show the WebContents. Simulate the "hide window with visible WebView" step
  // simply by detaching the WebContents.
  web_view()->SetVisible(true);
  EXPECT_EQ(2, observer1.shown_count());
  web_view()->holder()->Detach();
  EXPECT_EQ(2, observer1.hidden_count());
#endif
  // Case 5: Test that moving from a hidden parent to a visible parent makes the
  // web contents visible.
  Widget* parent2 = CreateTopLevelFramelessPlatformWidget();
  parent2->SetBounds(gfx::Rect(0, 10, 100, 100));
  parent2->Show();
  EXPECT_EQ(2, observer1.shown_count());
  // Note: that reparenting the windows directly, after the windows have been
  // created, e.g., Widget::ReparentNativeView(widget, parent2), is not a
  // supported use case. Instead, move the WebView over.
  parent2->SetContentsView(web_view()->parent()->RemoveChildViewT(web_view()));
  EXPECT_EQ(3, observer1.shown_count());
  parent2->Close();
}

// Verifies that there is no crash in WebView destructor
// if WebView is already removed from Widget.
TEST_F(WebViewUnitTest, DetachedWebViewDestructor) {
  // Init WebView with attached NativeView.
  const std::unique_ptr<content::WebContents> web_contents =
      CreateWebContents();
  View* contents_view = top_level_widget()->GetContentsView();
  auto* web_view = contents_view->AddChildView(
      std::make_unique<WebView>(web_contents->GetBrowserContext()));

  // Remove WebView from views hierarchy. NativeView should be detached
  // from Widget, and the WebView should be subsequently destroyed with no
  // crash.
  contents_view->RemoveChildViewT(web_view);
}

// Test that the specified crashed overlay view is shown when a WebContents
// is in a crashed state.
TEST_F(WebViewUnitTest, CrashedOverlayView) {
  const std::unique_ptr<content::WebContents> web_contents =
      CreateTestWebContents();

  View* contents_view = top_level_widget()->GetContentsView();
  auto* web_view = contents_view->AddChildView(
      std::make_unique<WebView>(web_contents->GetBrowserContext()));
  web_view->SetWebContents(web_contents.get());

  auto crashed_overlay_view = std::make_unique<View>();
  crashed_overlay_view->set_owned_by_client(View::OwnedByClientPassKey());
  web_view->SetCrashedOverlayView(crashed_overlay_view.get());
  EXPECT_FALSE(crashed_overlay_view->IsDrawn());

  SimulateRendererCrash(web_contents.get(), web_view);
  EXPECT_TRUE(crashed_overlay_view->IsDrawn());

  web_view->SetCrashedOverlayView(nullptr);
}

// Tests to make sure we can default construct the WebView class and set the
// BrowserContext after construction.
TEST_F(WebViewUnitTest, DefaultConstructability) {
  auto browser_context = std::make_unique<content::TestBrowserContext>();
  auto web_view = std::make_unique<WebView>();

  // Test to make sure the WebView returns a nullptr in the absence of an
  // explicitly supplied WebContents and BrowserContext.
  EXPECT_EQ(nullptr, web_view->GetWebContents());

  web_view->SetBrowserContext(browser_context.get());

  // WebView should be able to create a WebContents object from the previously
  // set |browser_context|.
  auto* web_contents = web_view->GetWebContents();
  EXPECT_NE(nullptr, web_contents);
  EXPECT_EQ(browser_context.get(), web_contents->GetBrowserContext());
}

// Tests that when a web view is reparented to a different widget hierarchy its
// holder's parent NativeViewAccessible matches that of its parent view's
// NativeViewAccessible.
TEST_F(WebViewUnitTest, ReparentingUpdatesParentAccessible) {
  const std::unique_ptr<content::WebContents> web_contents =
      CreateWebContents();
  auto web_view = std::make_unique<WebView>(web_contents->GetBrowserContext());
  web_view->SetWebContents(web_contents.get());

  WidgetAutoclosePtr widget_1(CreateTopLevelPlatformWidget());
  View* contents_view_1 = widget_1->GetContentsView();
  WebView* added_web_view = contents_view_1->AddChildView(std::move(web_view));

  // After being added to the widget hierarchy the holder's NativeViewAccessible
  // should match that of the web view's parent view.
  EXPECT_EQ(added_web_view->parent()->GetNativeViewAccessible(),
            added_web_view->holder()->GetParentAccessible());

  WidgetAutoclosePtr widget_2(CreateTopLevelPlatformWidget());
  View* contents_view_2 = widget_2->GetContentsView();

  // Reparent the web view. During reparenting, the holder should not return
  // a reference to the old parent's accessible object.
  std::unique_ptr<WebView> removed_view =
      contents_view_1->RemoveChildViewT(added_web_view);
  EXPECT_EQ(gfx::NativeViewAccessible(),
            added_web_view->holder()->GetParentAccessible());
  added_web_view = contents_view_2->AddChildView(std::move(removed_view));

  // After reparenting the holder's NativeViewAccessible should match that of
  // the web view's new parent view.
  EXPECT_EQ(added_web_view->parent()->GetNativeViewAccessible(),
            added_web_view->holder()->GetParentAccessible());
}

// This tests that we don't crash if WebView doesn't have a Widget or a
// Webcontents. https://crbug.com/1191999
// TODO(crbug.com/40923654): Re-enable this test
#if BUILDFLAG(IS_LINUX)
#define MAYBE_ChangeAXMode DISABLED_ChangeAXMode
#else
#define MAYBE_ChangeAXMode ChangeAXMode
#endif
TEST_F(WebViewUnitTest, MAYBE_ChangeAXMode) {
  // Case 1: WebView has a Widget and no WebContents.
  SetAXModeForView(web_view(), ui::AXMode::kFirstModeFlag);

  // Case 2: WebView has no Widget and a WebContents.
  View* contents_view = top_level_widget()->GetContentsView();
  // Remove the view but make sure to delete it at the end of the test.
  auto scoped_view = contents_view->RemoveChildViewT(web_view());
  const std::unique_ptr<content::WebContents> web_contents =
      CreateWebContents();
  scoped_view->SetWebContents(web_contents.get());
  SetAXModeForView(scoped_view.get(), ui::AXMode::kFirstModeFlag);
  // No crash.
}

// Tests to make sure the WebView clears away the reference to its hosted
// WebContents object when its deleted.
TEST_F(WebViewUnitTest, WebViewClearsWebContentsOnDestruction) {
  std::unique_ptr<content::WebContents> web_contents = CreateWebContents();
  web_view()->SetWebContents(web_contents.get());
  EXPECT_EQ(web_contents.get(), web_view()->web_contents());
  web_contents.reset();
  EXPECT_EQ(nullptr, web_view()->web_contents());
}

TEST_F(WebViewUnitTest, AccessibleProperties) {
  const std::unique_ptr<content::WebContents> web_contents =
      CreateWebContents();
  auto web_view = std::make_unique<WebView>(web_contents->GetBrowserContext());
  web_view->SetWebContents(web_contents.get());

  ui::AXNodeData data;
  web_view->GetViewAccessibility().GetAccessibleNodeData(&data);
  EXPECT_EQ(data.role, ax::mojom::Role::kWebView);
}

}  // namespace views