chromium/chrome/browser/ui/ash/web_view/ash_web_view_impl_browsertest.cc

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

#include "chrome/browser/ui/ash/web_view/ash_web_view_impl.h"

#include <memory>
#include <string>

#include "ash/public/cpp/ash_web_view.h"
#include "ash/public/cpp/ash_web_view_factory.h"
#include "base/run_loop.h"
#include "base/scoped_observation.h"
#include "base/strings/stringprintf.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "content/public/browser/host_zoom_map.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/test/browser_test.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/views/view.h"
#include "ui/views/view_observer.h"
#include "ui/views/widget/widget.h"

namespace {

using ash::AshWebView;
using ash::AshWebViewFactory;

// Macros ----------------------------------------------------------------------

#define EXPECT_PREFERRED_SIZE(web_view_, expected_preferred_size_)         \
  {                                                                        \
    MockViewObserver mock;                                                 \
    base::ScopedObservation<views::View, views::ViewObserver> observation{ \
        &mock};                                                            \
    observation.Observe(static_cast<views::View*>(web_view_));             \
                                                                           \
    base::RunLoop run_loop;                                                \
    EXPECT_CALL(mock, OnViewPreferredSizeChanged)                          \
        .WillOnce(testing::Invoke([&](views::View* view) {                 \
          EXPECT_EQ(expected_preferred_size_, view->GetPreferredSize());   \
          run_loop.QuitClosure().Run();                                    \
        }));                                                               \
    run_loop.Run();                                                        \
  }

#define EXPECT_DID_STOP_LOADING(web_view_)                                     \
  {                                                                            \
    MockAshWebViewObserver mock;                                               \
    base::ScopedObservation<AshWebView, AshWebView::Observer> obs{&mock};      \
    obs.Observe(web_view_);                                                    \
                                                                               \
    base::RunLoop run_loop;                                                    \
    EXPECT_CALL(mock, DidStopLoading).WillOnce(testing::Invoke([&run_loop]() { \
      run_loop.QuitClosure().Run();                                            \
    }));                                                                       \
    run_loop.Run();                                                            \
  }

#define EXPECT_DID_SUPPRESS_NAVIGATION(web_view_, expected_url_,          \
                                       expected_disposition_,             \
                                       expected_from_user_gesture_)       \
  {                                                                       \
    MockAshWebViewObserver mock;                                          \
    base::ScopedObservation<AshWebView, AshWebView::Observer> obs{&mock}; \
    obs.Observe(web_view_);                                               \
                                                                          \
    base::RunLoop run_loop;                                               \
    EXPECT_CALL(mock, DidSuppressNavigation)                              \
        .WillOnce(testing::Invoke([&](const GURL& url,                    \
                                      WindowOpenDisposition disposition,  \
                                      bool from_user_gesture) {           \
          EXPECT_EQ(expected_url_, url);                                  \
          EXPECT_EQ(expected_disposition_, disposition);                  \
          EXPECT_EQ(expected_from_user_gesture_, from_user_gesture);      \
          run_loop.QuitClosure().Run();                                   \
        }));                                                              \
    run_loop.Run();                                                       \
  }

#define EXPECT_DID_CHANGE_CAN_GO_BACK(web_view_, expected_can_go_back_)   \
  {                                                                       \
    MockAshWebViewObserver mock;                                          \
    base::ScopedObservation<AshWebView, AshWebView::Observer> obs{&mock}; \
    obs.Observe(web_view_);                                               \
                                                                          \
    base::RunLoop run_loop;                                               \
    EXPECT_CALL(mock, DidChangeCanGoBack)                                 \
        .WillOnce(testing::Invoke([&](bool can_go_back) {                 \
          EXPECT_EQ(expected_can_go_back_, can_go_back);                  \
          run_loop.QuitClosure().Run();                                   \
        }));                                                              \
    run_loop.Run();                                                       \
  }

// Helpers ---------------------------------------------------------------------

std::unique_ptr<views::Widget> CreateWidget() {
  auto widget = std::make_unique<views::Widget>();

  views::Widget::InitParams params(
      views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
      views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
  widget->Init(std::move(params));
  return widget;
}

GURL CreateDataUrlWithBody(const std::string& body) {
  return GURL(base::StringPrintf(R"(data:text/html,
      <html>
        <body>
          <style>
            * {
              margin: 0;
              padding: 0;
            }
          </style>
          %s
        </body>
      </html>
    )",
                                 body.c_str()));
}

GURL CreateDataUrl() {
  return CreateDataUrlWithBody(std::string());
}

// Mocks -----------------------------------------------------------------------

class MockViewObserver : public testing::NiceMock<views::ViewObserver> {
 public:
  // views::ViewObserver:
  MOCK_METHOD(void,
              OnViewPreferredSizeChanged,
              (views::View * view),
              (override));
};

class MockAshWebViewObserver : public testing::NiceMock<AshWebView::Observer> {
 public:
  // AshWebView::Observer:
  MOCK_METHOD(void, DidStopLoading, (), (override));

  MOCK_METHOD(void,
              DidSuppressNavigation,
              (const GURL& url,
               WindowOpenDisposition disposition,
               bool from_user_gesture),
              (override));

  MOCK_METHOD(void, DidChangeCanGoBack, (bool can_go_back), (override));
};

}  // namespace

// AshWebViewImplBrowserTest
// -------------AshWebViewImpl--------------------------------

class AshWebViewImplBrowserTest : public InProcessBrowserTest {
 public:
  AshWebViewImplBrowserTest() = default;
  AshWebViewImplBrowserTest(const AshWebViewImplBrowserTest&) = delete;
  AshWebViewImplBrowserTest& operator=(const AshWebViewImplBrowserTest&) =
      delete;
  ~AshWebViewImplBrowserTest() override = default;
};

// Tests -----------------------------------------------------------------------

// Tests that AshWebViewImpl will automatically update its preferred size
// to match the desired size of its hosted contents.
IN_PROC_BROWSER_TEST_F(AshWebViewImplBrowserTest, ShouldAutoResize) {
  AshWebView::InitParams params;
  params.enable_auto_resize = true;
  params.min_size = gfx::Size(600, 400);
  params.max_size = gfx::Size(800, 600);

  auto widget = CreateWidget();
  AshWebView* web_view =
      widget->SetContentsView(AshWebViewFactory::Get()->Create(params));

  // Verify auto-resizing within min/max bounds.
  web_view->Navigate(
      CreateDataUrlWithBody("<div style='width:700px; height:500px'></div>"));
  EXPECT_PREFERRED_SIZE(web_view, gfx::Size(700, 500));

  // Verify auto-resizing clamps to min bounds.
  web_view->Navigate(
      CreateDataUrlWithBody("<div style='width:0; height:0'></div>"));
  EXPECT_PREFERRED_SIZE(web_view, gfx::Size(600, 400));

  // Verify auto-resizing clamps to max bounds.
  web_view->Navigate(
      CreateDataUrlWithBody("<div style='width:1000px; height:1000px'></div>"));
  EXPECT_PREFERRED_SIZE(web_view, gfx::Size(800, 600));
}

// Tests that AshWebViewImpl will notify DidStopLoading() events.
IN_PROC_BROWSER_TEST_F(AshWebViewImplBrowserTest, ShouldNotifyDidStopLoading) {
  auto widget = CreateWidget();
  AshWebView* web_view = widget->SetContentsView(
      AshWebViewFactory::Get()->Create(AshWebView::InitParams()));

  web_view->Navigate(CreateDataUrl());
  EXPECT_DID_STOP_LOADING(web_view);
}

// Tests that AshWebViewImpl will notify DidSuppressNavigation() events.
IN_PROC_BROWSER_TEST_F(AshWebViewImplBrowserTest,
                       ShouldNotifyDidSuppressNavigation) {
  AshWebView::InitParams params;
  params.suppress_navigation = true;

  auto widget = CreateWidget();
  AshWebView* web_view = widget->SetContentsView(
      AshWebViewFactory::Get()->Create(std::move(params)));

  web_view->Navigate(CreateDataUrlWithBody(R"(
      <script>
        // Wait until window has finished loading.
        window.addEventListener("load", () => {

          // Perform simple click on an anchor within the same target.
          const anchor = document.createElement("a");
          anchor.href = "https://google.com/";
          anchor.click();

          // Wait for first click event to be flushed.
          setTimeout(() => {

            // Perform simple click on an anchor with "_blank" target.
            const anchor = document.createElement("a");
            anchor.href = "https://assistant.google.com/";
            anchor.target = "_blank";
            anchor.click();
          }, 0);
        });
      </script>
    )"));

  // Expect suppression of the first click event.
  EXPECT_DID_SUPPRESS_NAVIGATION(
      web_view, /*url=*/GURL("https://google.com/"),
      /*disposition=*/WindowOpenDisposition::CURRENT_TAB,
      /*from_user_gesture=*/false);

  // Expect suppression of the second click event.
  EXPECT_DID_SUPPRESS_NAVIGATION(
      web_view, /*url=*/GURL("https://assistant.google.com/"),
      /*disposition=*/WindowOpenDisposition::NEW_FOREGROUND_TAB,
      /*from_user_gesture=*/true);
}

// Tests that AshWebViewImpl will notify DidChangeCanGoBack() events.
IN_PROC_BROWSER_TEST_F(AshWebViewImplBrowserTest,
                       ShouldNotifyDidChangeCanGoBack) {
  auto widget = CreateWidget();
  AshWebView* web_view = widget->SetContentsView(
      AshWebViewFactory::Get()->Create(AshWebView::InitParams()));

  web_view->Navigate(CreateDataUrlWithBody("<div>First Page</div>"));
  EXPECT_DID_STOP_LOADING(web_view);

  web_view->Navigate(CreateDataUrlWithBody("<div>Second Page</div>"));
  EXPECT_DID_CHANGE_CAN_GO_BACK(web_view, /*can_go_back=*/true);

  web_view->GoBack();
  EXPECT_DID_CHANGE_CAN_GO_BACK(web_view, /*can_go_back=*/false);
}

IN_PROC_BROWSER_TEST_F(AshWebViewImplBrowserTest,
                       ShouldSetTemporaryZoomLevelToOne) {
  auto widget = CreateWidget();
  AshWebView::InitParams init_params;
  init_params.fix_zoom_level_to_one = true;

  AshWebView* web_view =
      widget->SetContentsView(AshWebViewFactory::Get()->Create(init_params));
  AshWebViewImpl* web_view_impl = static_cast<AshWebViewImpl*>(web_view);

  web_view->Navigate(CreateDataUrlWithBody("Hello"));
  EXPECT_DID_STOP_LOADING(web_view);

  // Confirm that temporary zoom level is set to 1.
  content::HostZoomMap* zoom_map =
      content::HostZoomMap::GetForWebContents(web_view_impl->web_contents());
  content::RenderFrameHost* render_frame_host =
      web_view_impl->web_contents()->GetPrimaryMainFrame();
  EXPECT_TRUE(
      zoom_map->UsesTemporaryZoomLevel(render_frame_host->GetGlobalId()));
  EXPECT_DOUBLE_EQ(
      1.0, content::HostZoomMap::GetZoomLevel(web_view_impl->web_contents()));

  // Navigate to a different url to trigger a RenderFrameHost change.
  web_view->Navigate(CreateDataUrlWithBody("World"));
  EXPECT_DID_STOP_LOADING(web_view);
  ASSERT_NE(render_frame_host,
            web_view_impl->web_contents()->GetPrimaryMainFrame());

  // Confirm that temporary zoom level is still applied even if RenderFrameHost
  // gets changed.
  zoom_map =
      content::HostZoomMap::GetForWebContents(web_view_impl->web_contents());
  render_frame_host = web_view_impl->web_contents()->GetPrimaryMainFrame();
  EXPECT_TRUE(
      zoom_map->UsesTemporaryZoomLevel(render_frame_host->GetGlobalId()));
  EXPECT_DOUBLE_EQ(
      1.0, content::HostZoomMap::GetZoomLevel(web_view_impl->web_contents()));
}