chromium/content/browser/display_cutout/safe_area_insets_host_impl_unittest.cc

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

#include "content/browser/display_cutout/safe_area_insets_host_impl.h"

#include <optional>

#include "base/memory/raw_ptr.h"
#include "base/test/scoped_feature_list.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/features.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_renderer_host.h"
#include "content/test/test_web_contents.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"

namespace content {

class TestSafeAreaInsetsHostImpl : public SafeAreaInsetsHostImpl {
 public:
  explicit TestSafeAreaInsetsHostImpl(WebContentsImpl* web_contents_impl)
      : SafeAreaInsetsHostImpl(web_contents_impl) {}

  void ResetSafeAreaTracking() { safe_area_insets_ = std::nullopt; }

  // Override to allow test access.
  void ViewportFitChangedForFrame(RenderFrameHost* rfh,
                                  blink::mojom::ViewportFit value) override {
    SafeAreaInsetsHostImpl::ViewportFitChangedForFrame(rfh, value);
  }

  using SafeAreaInsetsHostImpl::GetValueOrDefault;
  using SafeAreaInsetsHostImpl::SetViewportFitValue;

  bool did_send_safe_area() { return safe_area_insets_.has_value(); }
  gfx::Insets safe_area_insets() { return safe_area_insets_.value(); }

  RenderFrameHost* active_rfh() { return ActiveRenderFrameHost(); }

 protected:
  // Send the safe area insets to a `RenderFrameHost`.
  void SendSafeAreaToFrame(RenderFrameHost* rfh, gfx::Insets insets) override {
    safe_area_insets_ = insets;
    SafeAreaInsetsHostImpl::SendSafeAreaToFrame(rfh, insets);
  }

 private:
  std::optional<gfx::Insets> safe_area_insets_;
};

class SafeAreaInsetsHostImplTest : public RenderViewHostTestHarness {
 protected:
  void SetUp() override {
    RenderViewHostTestHarness::SetUp();
    SetContents(CreateTestWebContents());

    std::unique_ptr<TestSafeAreaInsetsHostImpl>
        test_safe_area_insets_host_impl =
            absl::make_unique<TestSafeAreaInsetsHostImpl>(test_web_contents());
    test_safe_area_insets_host_ = raw_ptr<TestSafeAreaInsetsHostImpl>(
        test_safe_area_insets_host_impl.get());
    test_web_contents()->SetSafeAreaInsetsHost(
        std::move(test_safe_area_insets_host_impl));
  }

  TestWebContents* test_web_contents() const {
    return static_cast<TestWebContents*>(web_contents());
  }

  TestSafeAreaInsetsHostImpl* test_safe_area_insets_host() const {
    return test_safe_area_insets_host_;
  }

  void ResetSafeAreaTracking() {
    test_safe_area_insets_host()->ResetSafeAreaTracking();
  }

  blink::mojom::ViewportFit GetValueOrDefault() {
    return test_safe_area_insets_host()->GetValueOrDefault(main_rfh());
  }

  void SetViewportFitValue(blink::mojom::ViewportFit value) {
    return test_safe_area_insets_host()->SetViewportFitValue(main_rfh(), value);
  }

  void NavigateToCover() {
    FocusWebContentsOnMainFrame();
    NavigateAndCommit(GURL("https://www.viewportFitCover.com"));
    test_safe_area_insets_host()->ViewportFitChangedForFrame(
        main_rfh(), blink::mojom::ViewportFit::kCover);
    // Simulate window insets changing, e.g. java's
    // DisplayCutoutController#onSafeAreaChanged notified from InsetObserver.
    test_web_contents()->SetDisplayCutoutSafeArea(gfx::Insets(42));
  }

  void NavigateToAuto() {
    FocusWebContentsOnMainFrame();
    NavigateAndCommit(GURL("https://www.viewportFitAuto.com"));
    test_safe_area_insets_host()->ViewportFitChangedForFrame(
        main_rfh(), blink::mojom::ViewportFit::kAuto);
    test_web_contents()->SetDisplayCutoutSafeArea(gfx::Insets(0));
  }

  void ExpectAuto() {
    EXPECT_EQ(blink::mojom::ViewportFit::kAuto, GetValueOrDefault())
        << "Not in Auto? Expected the main_rfh() to have kAuto in UserData, "
           "but it is not!";
  }

  void ExpectCover() {
    EXPECT_EQ(blink::mojom::ViewportFit::kCover, GetValueOrDefault())
        << "Not in Cover? Expected the main_rfh() to have kCover in UserData, "
           "but it is not!";
  }

 private:
  raw_ptr<TestSafeAreaInsetsHostImpl> test_safe_area_insets_host_;
};

TEST_F(SafeAreaInsetsHostImplTest, AutoToCover) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitWithFeatures(
      /*enabled_features=*/{features::kDrawCutoutEdgeToEdge},
      /*disabled_features=*/{});

  ResetSafeAreaTracking();
  NavigateToAuto();
  ExpectAuto();
  EXPECT_TRUE(test_safe_area_insets_host()->did_send_safe_area())
      << "The Insets should always be sent when they change.";
  EXPECT_EQ(0, test_safe_area_insets_host()->safe_area_insets().top())
      << "No Display Cutout, so the top inset should have been zero";

  ResetSafeAreaTracking();
  NavigateToCover();
  ExpectCover();
  EXPECT_TRUE(test_safe_area_insets_host()->did_send_safe_area())
      << "The Insets should always be sent when they change.";
  EXPECT_EQ(42, test_safe_area_insets_host()->safe_area_insets().top())
      << "The Display Cutout should have caused a non-zero top inset";
}

TEST_F(SafeAreaInsetsHostImplTest, CoverToAuto) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitWithFeatures(
      /*enabled_features=*/{features::kDrawCutoutEdgeToEdge},
      /*disabled_features=*/{});

  ResetSafeAreaTracking();
  NavigateToCover();
  ExpectCover();
  EXPECT_TRUE(test_safe_area_insets_host()->did_send_safe_area())
      << "The Insets should always be sent when they change.";
  EXPECT_EQ(42, test_safe_area_insets_host()->safe_area_insets().top())
      << "The Display Cutout should have caused a non-zero top inset";

  ResetSafeAreaTracking();
  NavigateToAuto();
  ExpectAuto();
  EXPECT_TRUE(test_safe_area_insets_host()->did_send_safe_area())
      << "The Insets should always be sent when they change.";
  EXPECT_EQ(0, test_safe_area_insets_host()->safe_area_insets().top())
      << "No Display Cutout, so the top inset should have been zero";
}

TEST_F(SafeAreaInsetsHostImplTest, SetViewportFitValue) {
  SetViewportFitValue(blink::mojom::ViewportFit::kAuto);
  EXPECT_EQ(blink::mojom::ViewportFit::kAuto, GetValueOrDefault());
  SetViewportFitValue(blink::mojom::ViewportFit::kCover);
  EXPECT_EQ(blink::mojom::ViewportFit::kCover, GetValueOrDefault());
  // Setting to a default value (kAuto) after a non default value may be
  // optimized to not store anything, but we do not test that directly.
  SetViewportFitValue(blink::mojom::ViewportFit::kAuto);
  EXPECT_EQ(blink::mojom::ViewportFit::kAuto, GetValueOrDefault());
}

TEST_F(SafeAreaInsetsHostImplTest, GetValueOrDefault_ExpiredRfh) {
  base::WeakPtr<RenderFrameHostImpl> null_rfh;
  EXPECT_EQ(blink::mojom::ViewportFit::kAuto,
            test_safe_area_insets_host()->GetValueOrDefault(null_rfh.get()))
      << "Passing in a null pointer should return kAuto instead of crashing.";
}

TEST_F(SafeAreaInsetsHostImplTest, ActiveFrameInFullscreen) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitWithFeatures(
      /*enabled_features=*/{features::kDrawCutoutEdgeToEdge},
      /*disabled_features=*/{});

  ResetSafeAreaTracking();
  NavigateToCover();
  ExpectCover();

  auto* subframe = static_cast<RenderFrameHostImpl*>(
      RenderFrameHostTester::For(main_rfh())->AppendChild("subframe"));
  test_safe_area_insets_host()->DidAcquireFullscreen(subframe);

  EXPECT_EQ(subframe, test_safe_area_insets_host()->active_rfh());
  EXPECT_EQ(42, test_safe_area_insets_host()->safe_area_insets().top())
      << "The Display Cutout should have caused a non-zero top inset";

  // Exit fullscreen from sub frame.
  ResetSafeAreaTracking();
  test_safe_area_insets_host()->DidExitFullscreen();

  EXPECT_EQ(main_rfh(), test_safe_area_insets_host()->active_rfh());
  EXPECT_EQ(42, test_safe_area_insets_host()->safe_area_insets().top())
      << "The Display Cutout should have caused a non-zero top inset";
}

}  // namespace content